Compiladores: P ASCAL jr

Documentos relacionados
Tokens, Padroes e Lexemas

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

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

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

CAP. VI ANÁLISE SEMÂNTICA

LINGUAGEM LIVRE DE CONTEXTO GRAMÁTICA LIVRE DE CONTEXTO

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.

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

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

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

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

Compiladores. Análise Léxica

Autômatos e Linguagens

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

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)

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

Compiladores 02 Analise léxica

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

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

Introdução aos Compiladores

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

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

1. Percorrer a cadeia, a partir da esquerda até que o primeiro > seja encontrado. 2. Percorrer, então, de volta (para a esquerda) por sobre quaisquer relações (=) até que < seja encontrado. 3. O handle contém tudo à esquerda do primeiro > e à direita do <, incluindo quaisquer não-terminais presentes. No exemplo acima, o primeiro handle é dado pelo primeiro id encontrado que pode ser reduzido para o não-terminal E (segundo a gramática vista), seguido pelos próximos dois ids da sentença. A seguir, a sentença obtida ficaria $ E + E * E $; removendo-se os não-terminais e acrescentando-se as relações de precedência temos: $ < + < > $, indicando que a próxima redução deve ser realizada sobre o operador * (e seus respectivos operandos associados E* E ). Devido ao fato da sua implementação não ser trivial, a solução de implementação mais viável para este tipo de gramática é fazer uso de um gerador de analisadores sintáticos, como o YACC ou BISON. 4.4.3 Projeto de uma Gramática para um Analisador Sintático Preditivo Descendente Considerando o uso de analisadores sintáticos descendentes preditivo algumas preocupações quanto a gramática a ser utilizada, devem ser tomadas: eliminar ambigüidade, eliminar as recursões à esquerda e fatorar à esquerda a gramática. Analisando um Programa Simples A seguir, é apresentado um exemplo de um programa simples na linguagem P ASCAL jr : var: float N1, N2, M; int Ct; const: int Qtde = 10; func float _Media(float a,float b) float media; { media = (a+b)/2.0; return media; } main( ) { Ct = 0; do { print("digite duas notas:"); scanf(n1,n2); printl("media = ",_Media(N1,N2)); Ct ++; } while(ct!= Qtde); } Todo programa em P ASCAL jr respeita a seguinte estrutura: 31

[ Declaração de Variáveis e Constantes ] [ Declaração de Sub-Rotinas ] <Programa Principal> onde: [ ] indica seção opcional e <> indica seção obrigatória. Pode-se descrever esta estrutura na forma de uma regra de produção de uma GLC da seguinte forma, onde o não-terminal Programa será o axioma da gramática da linguagem P ASCAL jr : Programa AreaDecl AreaSubRot Principal Analisando a Seção de Declaração de Variáveis e Constantes Esta seção declara todas as variáveis e constantes utilizadas pelo programa. É possível a declaração de várias áreas de declaração de variáveis e/ou constantes simultaneamente. Um programa pode ainda não conter esta seção. AreaDecl AreaDeclVar AreaDecl AreaDeclConst AreaDecl ε AreaDeclVar prvar DoisPt DeclVars DeclVars Tipo ListaID PtVirg DeclVars DeclVars Tipo ListaID PtVirg DeclVars ε Tipo print prfloat prchar prstring prbool ListaID Identificador ListaID ListaID Virg Identificador ListaID ε AreaDeclConst prconst DoisPt DeclConsts DeclConsts Tipo ListaIDConst PtVirg DeclConsts DeclConsts Tipo ListaIDConst PtVirg DeclConsts ε ListaIDConst Identificador Atrib Valor ListaIDConst ListaIDConst Virg Identificador Atrib Valor ListaIDConst ε Valor OpAritSubt Numeros Numeros ConstChar ConstString prtrue prfalse Numeros NumeroInteiro NumeroReal Analisando a Seção de Declaração de Procedimentos e Funções A seção de declaração de procedimentos e funções declara todas as sub-rotinas utilizadas pelo programa. É possível a declaração de várias áreas de declaração de sub-rotinas simultaneamente. Um programa pode ainda não conter esta seção. 32

AreaSubRot AreaProc AreaSubRot AreaFunc AreaSubRot ε AreaProc prproc IdentSR AbrePar ListaParam FechaPar AreaDecl BlocoCom ListaParam Tipo Identificador ListaParam ε ListaParam Virg Tipo Identificador ListaParam ε AreaFunc prfunc Tipo IdentSR AbrePar ListaParam FechaPar AreaDecl BlocoCom Analisando o Programa Principal O programa principal é o ponto onde inicia-se a execução do código fonte. Ela é definida pela função main. Apesar da linguagem P ASCAL jr não permitir a passagem de parâmetros para esta função, ainda sim utilizar-se-ão os parênteses ( ) na sintaxe do comando meramente por uma questão didática. Esta seção é obrigatória em qualquer programa. Principal prmain AbrePar FechaPar BlocoCom Analisando um Bloco de Comandos Um bloco de comandos pode ser entendido como um comando composto por uma lista de outros comandos simples (ou outros blocos) podendo (em alguns casos) ser separados por ; e delimitados por { e }. Sendo assim: BlocoCom AbreChaves ListaCom FechaChaves ListaCom Comando ListaCom ε Comando Condicional RepetPre RepetPos PtVirg RepetCont Entrada PtVirg Saida PtVirg Atrib PtVirg SubRot PtVirg BlocoCom Retorno PtVirg ε Analisando o comando Atribuição Pode ser realizado através de 7 diferentes operadores: = atribuição simples += atribuição após adição (X+ = Y X = X + Y ) 33

-= atribuição após subtração (X = Y X = X Y ) *= atribuição após multiplicação (X = Y X = X Y ) /= atribuição após divisão (X/ = Y X = X/Y )obs.:não prevê divisão por zero ++ atribuição incremental (X + + X = X + 1) - - atribuição decremental (X X = X 1) Exemplo de uma gramática que reconhece esses comandos: Atrib Identificador SimbAtrib Expr Identificador SimbAtribSoma Expr Identificador SimbAtribSubt Expr Identificador SimbAtribMult Expr Identificador SimbAtribDivi Expr Identificador SimbIncr Identificador SimbDecr porém, temos problemas de fatoração. Fatorando à esquerda estas produções temos: Atrib Identificador Atrib Atrib SimbAtrib Expr SimbAtribSoma Expr SimbAtribSubt Expr SimbAtribMult Expr SimbAtribDivi Expr SimbIncr SimbDecr Analisando o comando Condicional O comando condicional (sem fatoração) ficaria: Condic prif AbrePar Expr FechaPar Comando prif AbrePar Expr FechaPar Comando prelse Comando e após fatoração teremos: Condic prif AbrePar Expr FechaPar Comando Condic Condic prelse Comando ε 34

Analisando os comandos de Repetição Os comandos de repetição podem ser reconhecidos por: RepetPos prdo ListaCom prwhile AbrePar Expr FechaPar RepetPre prwhile AbrePar Expr FechaPar Comando RepetCont prfor AbrePar Atrib PtVirg Expr PtVirg Atrib FechaPar Comando Analisando os comandos para chamada a Sub-Rotinas Os comandos para chamadas a sub-rotinas incluem o comando < Retorno > que deve ser usado nas chamadas a funções. SubRot IdentSR AbrePar ListaExpr FechaPar Retorno prreturn Expr Analisando os comandos de Entrada e Saída Para os comandos de entrada e saída temos: 35

Entrada prscanf AbrePar ListaVar FechaPar Saida prprint AbrePar ListaExpr FechaPar prprintl AbrePar ListaExpr FechaPar ListaExpr Expr ListaExpr ε ListaExpr Virg Expr ListaExpr ε Analisando Expressões Lógicas e Aritméticas Para descrever sentenças que formam expressões aritméticas compostas das cinco operações básicas (adição, subtração, multiplicação, divisão e potenciação), tendo como operandos: identificadores de variáveis e constantes, números inteiros e reais, chamadas a sub-rotinas e ainda permitir o uso de parênteses e do operador unário de sinal - ; a representação mais simples possível seria: ExprAr ExprAr OpAdic ExprAr ExprAr OpSubt ExprAr ExprAr OpMult ExprAr ExprAr OpDivi ExprAr ExprAr OpPote ExprAr AbrePar ExprAr FechaPar OpSubt ExprAr SubRot Identificador NumeroInteiro NumeroReal ConstCaracter ConstString Exercício: Montar a árvore gramatical para a expressão 2 (X 5.0) + 10/B Apesar de que, com esta gramática, é possível gerar qualquer expressão aritmética simples, esta não leva em consideração todas as restrições já estudadas para a implementação de reconhecedores de gramática TOP-DOWN. O primeiro problema que se percebe é o fato da gramática anterior não considerar a questão da precedência de operadores. Para resolver este problema deve-se inserir novos elementos não-terminais à gramática: ExprAr ExprAr OpAdic TermoAr ExprAr OpSubt TermoAr TermoAr TermoAr TermoAr OpMult FatorAr TermoAr OpDivi FatorAr FatorAr FatorAr FatorAr OpPote ElementoAr ElementoAr ElementoAr AbrePar ExprAr FechaPar 36

OpSubt ExprAr SubRot Identificador NumeroInteiro NumeroReal ConstCaracter ConstString Exercício: Montar a árvore gramatical para a expressão 2 (X 5.0) + 10/B A idéia é gerar os elementos de menor precedência mais próximos à raiz da árvore sintática e os de maior precedência, mais próximos às folhas. Novamente temos problemas com a solução proposta: recursão à esquerda. A nova gramática após realizado o processo (já estudado) de eliminação da recursão à esquerda, temos: ExprAr TermoAr ExprAr ExprAr OpAdic TermoAr ExprAr OpSubt TermoAr ExprAr ε TermoAr FatorAr TermoAr TermoAr OpMult FatorAr TermoAr OpDivi FatorAr TermoAr ε FatorAr ElementoAr FatorAr FatorAr OpPote ElementoAr FatorAr ε ElementoAr AbrePar ExprAr FechaPar OpSubt ExprAr SubRot Identificador NumeroInteiro NumeroReal ConstCaracter ConstString Exercício: Montar a árvore sintática para a expressão: 2 (X 5.0) + 10/B. Analisando Expressões Lógicas Uma expressão lógica é, na verdade, uma comparação entre resultados de expressões aritméticas, ou ainda, a união de duas expressões aritméticas através de um operador relacional. São possíveis ainda, expressões lógicas mais complexas através da união de duas expressões lógicas simples por operadores lógicos. Expr TermoLog Expr Ternario Ternario Interrog Expr DoisPt Expr ε Expr OpLogAnd TermoLog Expr OpLogOr TermoLog Expr OpLogXor TermoLog Expr ε TermoLog FatorLog TermoLog 37

TermoLog OpRelacMaior FatorLog OpRelacMenor FatorLog OpRelacMenorIgual FatorLog OpRelacMaiorIgual FatorLog OpRelacIgual FatorLog OpRelacDifer FatorLog ε FatorLog ExprAr OpLogNeg Expr prtrue prfalse e ainda, ElementoAr AbrePar Expr FechaPar. 4.4.4 Exercícios Propostos 1. Montar a árvore sintática da expressão (A + 5 == B/C 2) && D! A >= B 2. Criar a cláusula FIRST para as produções da gramática preditiva descendente como forma de verificar sua implementação. 3. Criar a árvore gramatical para o programa abaixo: var: float A, B, C; main() { scanf(a,b); C=A+B*2; print(c); } 4.4.5 Trabalho Prático #2 Implementar um módulo (sub-rotina) analisador sintático descendente para um protótipo de compilador para a linguagem P ASCAL jr vista em aula. Características: Do módulo parser: A sub-rotina retorna um flag indicando sucesso ou não da análise sintática. Utiliza os tokens provenientes do módulo scanner (já implementado) para simular a lista léxica. Implementa uma pilha de execução (física ou por recursividade) para análise das regras de produção da gramática vista. Do programa a ser criado: Abre um arquivo fonte para análise. 38

Executa a análise da produção axioma da gramática. 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 e-mail: rsilva@joinville.udesc.br ou professor.rogerio@gmail.com Obrigatoriamente o mesmo grupo do trabalho anterior. Valor do trabalho: 10.0 (25% da nota prática). Data de Entrega: A definir Punições: de 10% por erro sintático não analisado corretamente. de 12.5% por erro léxico não analisado corretamente. de 20% do valor do trabalho por dia de atraso. de 20% do valor do trabalho para a entrega não conforme dos arquivos e/ou formato 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. 39

40

Capítulo 5 Análise Semântica 5.1 Tradução Dirigida pela Sintaxe A idéia é associar informações aos símbolos gramaticais que representam a construção de uma LLC; tais informações são atributos dos símbolos segundo certas regras semânticas associadas às produções da gramática. Existem duas notações para associar regras semânticas às produções: definições dirigidas pela sintaxe e esquemas de tradução. Definições Dirigidas pela Sintaxe Uma definição dirigida pela sintaxe é uma GLC na qual cada símbolo gramatical possui um conjunto associado de atributos, particionados em dois subconjuntos: atributos sintetizados e herdados. Um atributo é dito ser sintetizado se seu valor foi obtido a partir da computação dos valores dos filhos daquele nó da árvore e dito ser herdado se foi obtido a partir da computação dos irmãos e pai do respectivo nó. Uma árvore gramatical mostrando os valores dos atributos a cada nó é denominada de uma árvore gramatical anotada ou decorada. Para cada produção do tipo A α, temos associado a ela uma regra semântica da forma b := f(c 1, c 2,..., c n ), onde f é uma função que: Ou b é um atributo sintetizado de A e c 1, c 2,..., c n são atributos pertencentes aos símbolos gramaticais da produção ou, b é um atributo herdado, pertencente a um dos símbolos gramaticais do lado direito de produção e c 1, c 2,..., c n são atributos pertencentes aos símbolos da produção. Numa definição dirigida pela sintaxe, assume-se que os terminais tenham apenas atributos sintetizados visto que para os mesmos não existem regras semânticas. Se, para todas as produções, só forem utilizados atributos sintetizados diz-se se tratar de uma definição S-atribuída. Exemplo: Seja a gramática abaixo responsável pelo funcionamento de uma calculadora simples e tendo como único atributo sintetizado o valor val responsável pelo armazenamento de um número inteiro, então as regras semânticas (para determinação do valor resultante de uma expressão aritmética) são: 41

Produção L E E E + T E T T T F T F F (E) F inteiro Regras Semânticas imprimir(e.val) E.val = E.val+T.val E.val = T.val T.val = T.val*F.val T.val = F.val F.val = E.val F.val = inteiro.lexema L E Imprimir(19) E.val = 15+4 = 19 E.val = 15 E + T T.val = 4 T.val = 3*5 = 15 T F F.val = 4 T.val = 3 T * F F.val = 5 4 Inteiro.Lexval = 4 F.val = 3 F 5 Inteiro.Lexval = 5 3 Inteiro.Lexval = 3 Figura 5.1: Exemplo de Árvore Decorada para a Expressão 3*5+4 Atributos herdados são convenientes para expressar a dependência de uma construção de linguagem de programação no contexto em que a mesma figurar. Por exemplo, podemos usar um atributo herdado para controlar se um identificador aparece ao lado esquerdo ou direito de um comando de atribuição a fim de decidirmos se é necessário usar o endereço ou o valor do mesmo. Exemplo: Declaração de variáveis: int x,y,z Produção D T L T int T float L L, id L id Regras Semânticas L.in = T.tipo T.tipo = inteiro T.tipo = real L.in = L.in incluir tipo(id, L.in) incluir tipo(id, L.in) Grafos de Dependência Se um atributo b a um nó da árvore depender de um atributo c, a regra semântica para b àquele nó precisa ser avaliada após a regra semântica para c. As interdependências entre os atributos herdados e sintentizados são delineadas através de um grafo de dependência. 42

Exemplo: Supondo que a produção A XY tenha uma regra semântica da forma A.a = f(x.x, Y.y), ou seja, o atributo sintentizado a em A depende dos atributos x e y. A representação para esta dependência em um grafo de dependência ficaria (conforme a figura 5.2): 1. Existem três nós A.a, X.x e Y.y 2. Existe um arco partindo de X.x para A.a pois a depende de x 3. Existe um arco partindo de Y.y para A.a pois a depende de y A.a X.x Y.y Figura 5.2: Grafo de Dependências 5.1.1 Definições L-Atribuídas O nome L provém de left, onde a análise gramatical é feita sempre a partir do filho mais à esquerda na árvore sintática através do seguinte algoritmo: Algoritmo de pesquisa em profundidade (depth-first order): Procedimento Visitar(n: nó); Inicio Para cada filho m de n da esquerda para a direita faça Inicio Avaliar os atributos herdados de m; Visitar(m); Fim Avaliar os atributos sintetizados de m; Fim Uma definição dirigida pela sintaxe é L-atribuída se cada atributo herdado de X j, i j n, do lado direito de A X 1, X 2,..., X n depender somente: Dos atributos dos símbolos X 1, X 2,..., X j 1 à esquerda de X j na produção e Dos atributos herdados de A. Note-se que cada definição S-atribuída é L-atribuída porque as restrições (1) e (2) se aplicam somente aos atributos herdados. 43

5.1.2 Verificações de Contexto Um compilador precisa fazer uma verificação estática das estruturas do programa, para assegurar que o mesmo esteja livre de certos tipos de erros, tais como: Verificação de Tipos verifica se um operador está sendo aplicado a operandos de tipos incompatíveis. Exemplo: 10 + TRUE. Verificação de Fluxo de Controle os enunciados de fluxo de controle (repetições, p. ex.) precisam ter algum local de retorno para onde transferir o controle. Exemplo: um comando tipo break em C precisa estar envolvido por algum comando de fluxo de controle (while, for ou switch). Verificações de Unicidade verifica se os identificadores foram declarados de forma unívoca, ou seja, sem duplicidade. Verificações relacionadas aos nomes existem linguagens que apresentam particularidades acerca dos identificadores do programa. Essa etapa da análise verifica se essas particularidades foram respeitadas. Exemplo: A linguagem ADA exige que blocos de comandos comecem e terminem com o mesmo identificador. Sistema de Tipos Define-se como um sistema de tipos ao conjunto de regras de aplicabilidade entre os tipos dos operandos e os operadores da linguagem. Exemplo: Seja o comando X = A + B. Um sistema de tipos iria validar se os tipos associados aos operandos A e B são válidos para a adição e, em caso afirmativo, qual seria o tipo resultante, e ainda, se esse tipo resultante é compatível com o tipo associado à variável X. Todo o processo de análise semântica para verificação de tipos é realizado a partir da construção de expressões de tipo, que podem ser compostas: ou por um tipo simples ou por um construtor de tipos (tipo resultante da aplicação de um operador). Voltando no exemplo: X = A+B; supor que X e B sejam declarados do tipo inteiro e A seja do tipo real, assim temos: = = = (inválido) X + int + int + (float) A B float int float int Tipo Simples Construção de Tipos Figura 5.3: Tipos Simples e Construtor de Tipos O processo de verificação de tipos consiste da aplicação de regras semânticas às produções da gramática sob análise. Usualmente, um atributo sintetizado tipo é suficiente para tal análise. 44

Exemplo1: Para uma produção do tipo E id teríamos uma regra semântica como { E.tipo = id.tipo }. Exemplo2: Imagine uma linguagem que permita apenas operações com tipos idênticos, então para uma produção do tipo E E + T teríamos a regra semântica { E.tipo = se E.tipo = T.tipo então E.tipo senão inválido } Verificação de Tipos em Expressões As seguintes regras semânticas podem ser abstraídas de forma geral: E tipo simples E id E E 1 E E 1 op E 2 {E.tipo = tipo simples} {E.tipo = ProcurarTS(id).tipo} {E.tipo = E 1.tipo} {E.tipo = SistemaT ipos[e 1.tipo, op, E 2, tipo]} Construção de um Sistema de Tipos para Operadores Apresenta as relações de compatibilidade, para cada operador, em função dos operandos. Exemplo na linguagem Pascal: Op 1 Op 2 + - * / integer integer integer integer integer real integer real real real real real integer char Erro Erro Erro Erro integer string Erro Erro Erro Erro integer boolean Erro Erro Erro Erro real integer real real real real real real real real real real real char Erro Erro Erro Erro real string Erro Erro Erro Erro real boolean Erro Erro Erro Erro char integer Erro Erro Erro Erro char real Erro Erro Erro Erro char char String Erro Erro Erro char string String Erro Erro Erro char boolean Erro Erro Erro Erro string integer Erro Erro Erro Erro string real Erro Erro Erro Erro string char String Erro Erro Erro string string String Erro Erro Erro string boolean Erro Erro Erro Erro boolean integer Erro Erro Erro Erro boolean real Erro Erro Erro Erro boolean char Erro Erro Erro Erro boolean string Erro Erro Erro Erro boolean boolean Erro Erro Erro Erro 45

Exercício: Construir as tabelas de sistema de tipos para todos os operadores utilizados na gramática da linguagem P ASCAL jr. Operadores Aritméticos: +, -, *, /, ** Operadores Relacionais: ==,!=, >, <, >=, <= Símbolos Atribuição: =, +=, -=, *=, /= Operadores Lógicos: &&,, & Operadores Unários: -,!, ++, - - Op 1 Op 2 + - * / ** inteiro inteiro inteiro inteiro inteiro real inteiro inteiro real real real real real real inteiro caracter Erro Erro Erro Erro Erro inteiro cadeia Erro Erro Erro Erro Erro inteiro logico Erro Erro Erro Erro Erro real inteiro real real real real real real real real real real real real real caracter Erro Erro Erro Erro Erro real cadeia Erro Erro Erro Erro Erro real logico Erro Erro Erro Erro Erro caracter inteiro Erro Erro Erro Erro Erro caracter real Erro Erro Erro Erro Erro caracter caracter cadeia Erro Erro Erro Erro caracter cadeia cadeia Erro Erro Erro Erro caracter logico Erro Erro Erro Erro Erro cadeia inteiro Erro Erro Erro Erro Erro cadeia real Erro Erro Erro Erro Erro cadeia caracter cadeia Erro Erro Erro Erro cadeia cadeia cadeia Erro Erro Erro Erro cadeia logico Erro Erro Erro Erro Erro logico inteiro Erro Erro Erro Erro Erro logico real Erro Erro Erro Erro Erro logico caracter Erro Erro Erro Erro Erro logico cadeia Erro Erro Erro Erro Erro logico logico Erro Erro Erro Erro Erro 5.2 Tabela de Símbolos É uma estrutura de dados gerada pelo compilador com o objetivo de armazenar informações sobre os nomes (identificadores de variáveis, de parâmetros, de funções, etc.) definidos no programa fonte. Ela associa atributos tais como tipo, escopo, tamanho, limite (no caso de vetores, número de parâmetros (no caso de subrotinas) a cada identificador armazenado na tabela. Em geral, uma tabela de símbolo pode começar a ser construída já na fase de análise léxica, mas geralmente nesta fase ainda não é possível se determinar as informações associadas aos atributos dos identificadores que foram reconhecidas pelo compilador. Assim 46

sendo, essas tarefas são realizadas nas fases de análise sintática e/ou semântica, onde se referencia à tabela, cada vez que um identificador for encontrado no programa. Os problemas enfrentados ao se projetar uma tabela de símbolos são: a quantidade de símbolos armazenados na tabela, depende do programa fonte sob análise, ou seja, é desejável uma estrutura dinâmica de alocação de memória para a tabela. Se uma estrutura estática for construída, esta deve ter um tamanho suficientemente grande para suportar qualquer programa (limitação de tamanho); a quantidade de acesso à tabela (inclusões e consultas), pode ser bastante grande, dependendo do programa. Pode-se organizar os dados na tabela através de listas lineares, árvores binárias ou tabelas hash. O mecanismo linear, apesar de simples, é ineficiente para programas grandes, já o hashing tem melhor desempenho mas exige maior esforço de programação. cada entrada na TS está associada a um nome de identificador. Estas entradas podem não ser uniformes, ou seja, os atributos de um identificador de variável não são os mesmos de um identificador de função, por exemplo. O uso de registros variantes pode ser uma alternativa elegante para se organizar a estrutura que conterá tais atributos. Enfim, ao se projetar uma tabela de símbolos deve se levar em conta: a quantidade de dados a serem armazenados, a natureza desses dados, o tempo de acesso e a facilidade de modificação da estrutura de armazenamento desses dados. Essas preocupações poderão estar relacionadas à aplicação que se deseja desenvolver. 5.2.1 Atributos dos Nomes dos Identificadores De maneira geral, qualquer informações acerca dos identificadores definidos em um programa fonte, podem (e devem) ser armazenados em uma tabela de símbolos. O conjunto de atributos é inerente às características da linguagem sendo reconhecida. Atributos como nome, tipo, uso no programa (isto é: variável, constante, procedimento, função, rótulo), tamanho de memória a ser alocada (em bytes), endereço (onde foi alocada), escopo, etc. Para efeito da disciplina (implementação do protótipo) apenas os atributos: nome, tipo, endereço, natureza (var ou const). 5.2.2 Hashing A função de espalhamento é responsável por determinar qual endereço (na tabela Hash), uma determinada chave k deve ser inserida. Exemplo: int Hash(int Key){return Key%K T AM HASH; } onde, K TAM HASH indica o tamanho (n de posições) do vetor hash. A função hash acima gera valores entre 0 e K TAM HASH-1 em função do valor de Key (chave a ser inserida). Problema!: Caso exista duas chaves que possuam o mesmo valor de chave, a função hash irá gerar o mesmo endereço de vetor. Isto é chamado de colisão ou conflito de espalhamento. 47

Existem dois métodos básicos para manipular colisões de espalhamento: reespalhamento ou encadeamento. Solucionando colisões através de reespalhamento Requer o uso de uma função de espalhamento secundária sobre a chave, sucessivas vezes até que um endereço válido (disponível) seja encontrado para inserção do elemento. No processo de busca ocorre idéia semelhante, exemplo: desejase localizar uma determinada chave k: 1. usa-se a função de espalhamento principal 2. caso não encontrado, usa-se a função de espalhamento secundária 3. caso não encontrado, repete-se o passo 2. 1. Inserindo elemento MEDIA: Hash(MEDIA)=6 1 2 3 4 5 6 MEDIA 2. Inserindo elemento X: Hash(X)=2 1 2 X 3 4 5 6 MEDIA 3. Inserindo elemento MEDIA FINAL: Hash(MEDIA FINAL)=6 (conflito!) Hash2(MEDIA FINAL)=1 (endereço válido) 1 MEDIA FINAL 2 X 3 4 5 6 MEDIA 4. Inserindo elemento CONT: Hash(CONT)=1 (conflito!) Hash2(CONT)=2 (conflito!) Hash2(CONT)=3 (endereço válido) 1 MEDIA FINAL 2 X 3 CONT 4 5 6 MEDIA 48

O método mais simples de solucionar colisões de espalhamento é colocar o registro na próxima posição disponível no vetor. OBS.: Deve-se trabalhar os dados no vetor da mesma forma que em uma lista circular, ou seja, a próxima posição após o último elemento é novamente o primeiro elemento. Solucionando colisões através de encadeamento Usa-se uma lista encadeada para armazenar os elementos conflitantes no local do conflito. Cada posição do vetor é o inicio de uma lista de elementos. Sempre que um conflito na inserção de dados ocorrer, o novo elemento (conflitante) é inserido em uma lista encadeada na posição. Media X1 X2 A Res B C Cont Figura 5.4: Hashing com Encadeamento 49

5.3 Projeto das Regras Semânticas Os símbolos semânticos no protótipo P ASCAL jr armazenarão as seguintes informações: Lexema, Tipo, Endereço, Natureza (VAR ou CONST ), Valor (No caso de constantes). O TDA (Hashing) é acessado através dos seguintes métodos: CriaTS aloca a tabela de símbolos vazia InsereTS insere um novo símbolo na tabela de símbolos BuscaTS retorna as informações de um determinado símbolo na tabela DestroiTS desaloca toda a tabela de símbolos Os possíveis erros semânticos são: 1. Identificador Não Declarado 2. Identificador de Variável Esperado 3. Tipos Incompatíveis 4. Identificador Duplicado 5. Retorno de Função Esperado 6. Uso Incorreto de Chamada de Sob-Rotina As seguintes regras semânticas serão aplicadas às regras da gramática da linguagem P ASCAL jr. Obs.: Não esquecer de considerar os grafos de dependência entre os atributos semânticos. 50

Programa AreaDecl AreaSubRot Principal { CriaTS(); EnderecoLivre=0; } AreaDecl AreaDeclVar AreaDecl AreaDeclConst AreaDecl ε { Nenhuma Ação Semântica } AreaDeclVar prvar DoisPt DeclVars { Nenhuma Ação Semântica } DeclVars Tipo ListaID PtVirg DeclVars { ListaID.Acao = DeclV ar; ListaID.T ipo = Tipo.T ipo } DeclVars Tipo ListaID PtVirg DeclVars { ListaID.Acao = DeclV ar; ListaID.T ipo = Tipo.T ipo } DeclVars ε { Nenhuma Ação Semântica } Tipo print { Tipo.T ipo = Inteiro } Tipo prfloat { Tipo.T ipo = Real } Tipo prchar { Tipo.T ipo = Caracter } Tipo prstring { Tipo.T ipo = Cadeia } Tipo prbool { Tipo.T ipo = Logico } ListaID Identificador ListaID { ListaID.Acao = ListaID.Acao; ListaID.T ipo = ListaID.T ipo; if(listaid.acao==declvar){ if(!inseret S(VAR, Identificador.Lexema, ListaID.T ipo, EnderecoLivre + +) ERRO SEM 4; } else Entrada=BuscaTS(Identificador.Lexema); if(!entrada) ERRO SEM 1 else if(entrada.natureza!= VAR) ERRO SEM 2 } ListaID 1 Virg Identificador ListaID 2 { ListaID 2.Acao = ListaID 1.Acao; ListaID 2.T ipo = ListaID 1.T ipo; if(listaid 1.Acao==DeclVar){ if(!inseret S(VAR, Identificador.Lexema, ListaID.T ipo, EnderecoLivre + +) ERRO SEM 4; } else Entrada=BuscaTS(Identificador.Lexema); if(!entrada) ERRO SEM 1 else if(entrada.natureza!= VAR) ERRO SEM 2 } ListaID ε { Nenhuma Ação Semântica } AreaDeclConst prconst DoisPt DeclConsts { Nenhuma Ação Semântica } DeclConsts Tipo ListaIDConst PtVirg DeclConsts { ListaIDConst.T ipo = Tipo.T ipo } DeclConsts Tipo ListaIDConst PtVirg DeclConsts { ListaIDConst.T ipo = Tipo.T ipo } DeclConsts ε { Nenhuma Ação Semântica } ListaIDConst Identificador Atrib Valor ListaIDConst { ListaIDConst.T ipo = ListaIDConst.T ipo; InsereT S(CONST, Identificador.Lexema, ListaIDConst.T ipo, EnderecoLivre + +, Valor.V alor) } 51

ListaIDConst 1 Virg Identificador Atrib Valor ListaIDConst 2 { ListaIDConst 2.T ipo = ListaIDConst 1.T ipo; InsereT S(CONST, Identificador.Lexema, ListaIDConst 1.T ipo, EnderecoLivre + +, Valor.V alor) } ListaIDConst ε { Nenhuma Ação Semântica } Valor OpAritSubt Numeros { Valor.V alor = Numeros.V alor } Valor Numeros { Valor.V alor = Numeros.V alor } Valor ConstCaracter { Valor.V alor = ConstCaracter.Lexema } Valor ConstString { Valor.V alor = ConstString.Lexema } Valor prtrue { Valor.V alor = true } Valor prfalse { Valor.V alor = false } Numeros NumeroInteiro { Numeros.V alor = NumeroInteiro.Lexema } Numeros NumeroReal { Numeros.V alor = NumeroReal.Lexema } BlocoCom AbreChaves ListaCom FechaChaves { Nenhuma Ação Semântica } ListaCom ComSimp ListaCom { Nenhuma Ação Semântica } ListaCom ε { Nenhuma Ação Semântica } ListaCom PtVirg ComSimp ListaCom { Nenhuma Ação Semântica } < ListaCom > ε { Nenhuma Ação Semântica } < ComSimp > < Atrib > { Nenhuma Ação Semântica } < ComSimp > < RepetPre > { Nenhuma Ação Semântica } < ComSimp > < RepetPos > { Nenhuma Ação Semântica } < ComSimp > < RepetCont > { Nenhuma Ação Semântica } < ComSimp > < Entrada > { Nenhuma Ação Semântica } < ComSimp > < Saida > { Nenhuma Ação Semântica } < ComSimp > < Condic > { Nenhuma Ação Semântica } < ComSimp > < BlocoCom > { Nenhuma Ação Semântica } < ComSimp > ε { Nenhuma Ação Semântica } < Atrib > Identificador < Atrib > {Entrada = BuscaT S(Identificador); if(!entrada)erro SEM 1 else if(entrada.natureza! = V AR)ERRO SEM 2; < Atrib >.T ipo = Entrada.T ipo;} 52

< Atrib > SimbAtrib < Expr > {if(!sistemat ipos(< Atrib >.T ipo, SimbAtrib, < Expr >.T ipo))erro SEM 3} < Atrib > SimbAtribSoma < Expr > {if(!sistemat ipos(< Atrib >.T ipo, SimbAtribSoma, < Expr >.T ipo))erro SEM 3} < Atrib > SimbAtribSubt < Expr > {if(!sistemat ipos(< Atrib >.T ipo, SimbAtribSubt, < Expr >.T ipo))erro SEM 3} < Atrib > SimbAtribMult < Expr > {if(!sistemat ipos(< Atrib >.T ipo, SimbAtribMult, < Expr >.T ipo))erro SEM 3} < Atrib > SimbAtribDivi < Expr > {if(!sistemat ipos(< Atrib >.T ipo, SimbAtribDivi, < Expr >.T ipo))erro SEM 3} < Atrib > SimbIncr {if(!sistemat ipos(< Atrib >.T ipo, SimbIncr))ERRO SEM 3} < Atrib > SimbDecr {if(!sistemat ipos(< Atrib >.T ipo, SimbDecr))ERRO SEM 3} < Condic > prse < Expr > prentao < ComSimp >< Condic > {if(< Expr >.T ipo! = Logico)ERRO SEM 3} < Condic > prsenao < ComSimp > { Nenhuma Ação Semântica } < Condic > ε { Nenhuma Ação Semântica } < RepetPos > prrepita < ListaCom > prate < Expr > {if(! < Expr >.T ipo == Logico)ERRO SEM 3} < RepetPre > prenquanto < Expr > prfaca < CompSimp > {if(! < Expr >.T ipo == Logico)ERRO SEM 3} < RepetCont > prpara Identificador SimbAtribValor > prate < Expr > prfaca < RepetCont >< ComSimp > {Entrada = BuscaT S(Identificador); if(entrada.t ipo! =< Valor >.T ipo Entrada.T ipo! = Inteiro)ERRO SEM 3 elseif(< Expr >.tipo! = inteiro)erro SEM 3 < RepetCont > prpasso < Expr > if(< Expr >.tipo! = inteiro)erro SEM 3 < RepetCont > ε { Nenhuma Ação Semântica } < Valor > NumeroInteiro { < Valor >.T ipo = inteiro } < Valor > Identificador { < Valor >.T ipo = BuscaT S(Identificador).T ipo } < Entrada > prleia AbrePar < ListaID > FechaPar { < ListaID >.Acao = Leia; } < Saida > primprima Abrepar < ListaExpr > FechaPar { Nenhuma Ação Semântica } 53

< ListaExpr > < Expr >< ListaExpr > { Nenhuma Ação Semântica } < ListaExpr > Virg < Expr >< ListaExpr > { Nenhuma Ação Semântica } < ListaExpr > ε { Nenhuma Ação Semântica } < Expr > < TermoLog >< Expr >< Ternario > {< Expr >.in =< TermoLog >.out; < Ternario >.in =< Expr >.out; < Expr >.tipo =< Ternario >.out; } < Ternario > Interrog < Expr > 1 DoisPt < Expr > 2 {if(< Ternario >.in! = Logico)ERRO SEM 3 else if(< Expr > 1.T ipo! = < Expr > 2.T ipo) ERRO SEM 3 else < Ternario >.out =< Expr > 1.T ipo} < Ternario > ε {< Ternario >.out =< Ternario >.in} < Expr > OpLogAnd < TermoLog >< Expr > 1 {< Expr > 1.in = SistemaT ipos[< Expr >.in, OpLogAnd, < TermoLog >.out]; < Expr >.out =< Expr > 1.out} < Expr > OpLogOr < TermoLog >< Expr > {< Expr > 1.in = SistemaT ipos[< Expr >.in, OpLogOr, < TermoLog >.out]; < Expr >.out =< Expr > 1.out} < Expr > OpLogXor < TermoLog >< Expr > {< Expr > 1.in = SistemaT ipos[< Expr >.in, OpLogXor, < TermoLog >.out]; < Expr >.out =< Expr > 1.out} < Expr > ε {< Expr >.out =< Expr >.in} < TermoLog > < FatorLog >< TermoLog > {< TermoLog >.in =< FatorLog >.out; < TermoLog >.out =< TermoLog >.out; } < TermoLog > OpRelacMaior < FatorLog > {< TermoLog >.out = SistemaT ipos[< TermoLog >.in, OpRelacMaior, < FatorLog >.out]; } < TermoLog > OpRelacMenor < FatorLog > {< TermoLog >.out = SistemaT ipos[< TermoLog >.in, OpRelacMenor, < FatorLog >.out]; } < TermoLog > OpRelacMenorIgual < FatorLog > {< TermoLog >.out = SistemaT ipos[< TermoLog >.in, OpRelacMenorIgual, < FatorLog >.out]; } < TermoLog > OpRelacMaiorIgual < FatorLog > {< TermoLog >.out = SistemaT ipos[< TermoLog >.in, OpRelacMaiorIgual, < FatorLog >.out]; } 54

< TermoLog > OpRelacIgual < FatorLog > {< TermoLog >.out = SistemaT ipos[< TermoLog >.in, OpRelacIgual, < FatorLog >.out]; } < TermoLog > OpRelacDifer < FatorLog > {< TermoLog >.out = SistemaT ipos[< TermoLog >.in, OpRelacDifer, < FatorLog >.out]; } < TermoLog > ε {< TermoLog >.out =< TermoLog >.in} < FatorLog > < ExprAr > {< FatorLog >.out =< ExprAr >.out} < FatorLog > OpLogNeg < Expr > {< FatorLog >.out = SistemaT ipos[< Expr >.out, OpLogNeg]; } < FatorLog > prverdadeiro {< FatorLog >.out = V erdadeiro} < FatorLog > prfalso {< FatorLog >.out = F also} < ExprAr > < TermoAr >< ExprAr > {< ExprAr >.in =< TermoAr >.out; < ExprAr >.out =< ExprAr >.out; } < ExprAr > OpAdic < TermoAr >< ExprAr > 1 {< ExprAr > 1.in = SistemaT ipos[< ExprAr >.in, OpAdic, < TermoAr >.out]; < ExprAr >.out =< ExprAr > 1.out; } < ExprAr > OpSubt < TermoAr >< ExprAr > 1 {< ExprAr > 1.in = SistemaT ipos[< ExprAr >.in, OpSubt, < TermoAr >.out]; < ExprAr >.out =< ExprAr > 1.out; } < ExprAr > ε {< ExprAr >.out =< ExprAr >.in} < TermoAr > < FatorAr >< TermoAr > {< TermoAr >.in =< FatorAr >.out; < TermoAr >.out =< TermoAr >.out; } < TermoAr > OpMult < FatorAr >< TermoAr > 1 {< TermoAr > 1.in = SistemaT ipos[< TermoAr >.in, OpMult, < FatorAr >.out]; < TermoAr >.out =< TermoAr > 1.out; } < TermoAr > OpDivi < FatorAr >< TermoAr > 1 {< TermoAr > 1.in = SistemaT ipos[< TermoAr >.in, OpDivi, < FatorAr >.out]; < TermoAr >.out =< TermoAr > 1.out; } < TermoAr > ε {< TermoAr >.out =< TermoAr >.in} < FatorAr > < ElementoAr >< FatorAr > 1 {< FatorAr > 1.in =< ElementoAr >.out; < FatorAr >.out =< FatorAr > 1.out; } 55

< FatorAr > OpPote < ElementoAr >< FatorAr > 1 {< FatorAr > 1.in = SistemaT ipos[< FatorAr >.in, OpPote, < ElementoAr >.out]; < FatorAr >.out =< FatorAr > 1.out; } < FatorAr > ε {< FatorAr >.out =< FatorAr >.in} < ElementoAr > AbrePar < Expr > FechaPar {< ElementoAr >.out =< Expr >.out; } < ElementoAr > OpSubt < ExprAr > {< ElementoAr >.out = SistemaT ipos[< ExprAr >.out, OpSubt]; } < ElementoAr > Identificador {< ElementoAr >.out = BuscaT S(Identificador).T ipo; } < ElementoAr > NumeroInteiro {< ElementoAr >.out = Inteiro} < ElementoAr > NumeroReal {< ElementoAr >.out = Real} < ElementoAr > ConstCaracter {< ElementoAr >.out = Caracter} < ElementoAr > ConstString {< ElementoAr >.out = Cadeia} 5.3.1 Trabalho Prático #3 Implementar um módulo analisador semântico para um protótipo de compilador para a linguagem P ASCAL jr (simplificada) vista em aula. Características: Do módulo semântico: Cada chamada a elementos não-terminais processa as ações semânticos necessárias para cada produção da gramática. Utiliza a árvore gramatical criada no módulo parser. Implementa uma tabela de símbolos (Hashing) a fim de armazenar os símbolos reconhecidos durante a compilação. Do programa a ser criado: Abre um arquivo fonte para análise. Executa a análise semântica a partir da produção axioma da gramática. 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: 56

Implementação usando linguagem C ou C++. Entrega de fontes e executável (em um disquete identificado). Grupo de 02 alunos (máximo). Valor do trabalho: 10.0 (25% da nota prática). Data de Entrega: 08/11/2004. Punições: de 20% do valor do trabalho por dia de atraso. de 20% por erro léxico não analisado corretamente. de 15% por erro sintático não analisado corretamente. de 10% por erro semântico não analisado corretamente. 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: 7 dias letivos após entrega. Punições: de 33% ponto por dia de atraso da defesa. de 25% para arguição não respondida ou respondida incorretamente. Obs.: A arguição é individual. 57

58

Capítulo 6 Geração de Código Intermediário É a primeira fase da etapa de síntese, responsável por transformar a árvore de derivação em um trecho de código (que pode eventualmente ser o próprio código objeto final). Freqüentemente porém, o código gerado não especifica detalhes da máquina alvo, tais como quais registradores serão usados ou quais endereços de memória serão referenciados, etc. Existem vantagens e desvantagens de se usar a etapa de geração de código intermediário. Vantagens: Permite otimizações de código, a fim de tornar o código final mais eficiente; Simplifica a implementação do compilador; Possibilita que um mesmo código intermediário possa ser traduzido para diferentes linguagens objeto. Desvantagens: Uma etapa a mais é executada durante o processo de compilação, tornando o processo mais lento. 6.1 Linguagens Intermediárias São divididas em 3 categorias: Representações gráficas; Notação pós (ou pré) fixadas; Código de três-endereços. 6.1.1 Representações Gráficas É uma forma condensada de árvore de derivação na qual somente os operandos da linguagem aparecem como folhas; os operadores constituem nós interiores da árvore (figura 6.1). Exercício: Gerar as representações gráfica para as expressões abaixo: 59

= a + * * b c b 2 Figura 6.1: Exemplo de Representação Gráfica de Operadores para a=b*c+b*2 1. a = b + c d/4 2. exp = b b 4 a c >= 0 && a! = 0 3. d = (a a) (b b) 6.1.2 Notação Pós (e Pré) Fixadas Dada uma certa expressão E1 q E2, onde E1 e E2 são os operandos e q um operador, a expressão pós-fixada é representada por E1 E2 q, enquanto que a representação pré-fixada por q E1 E2. Exemplos: Infixa Pós-Fixada Pré-Fixada (a + b) c ab + c + abc a (b + c) abc + a + bc a + b c abc + +a bc a = b c + d abc d+ = = a + bcd Exercícios: Gerar as notações pré e pós fixas das expressões abaixo: 1. a + a b + b 2. a + b 4/d c 3. (a + a) (b + b) 4. a = b + c d/4 5. exp = b b 4 a c >= 0 && a! = 0 6. d = (a a) (b b) A seguir é apresentado um esquema de tradução para expressões pós fixadas: E E1 + T {E.cod = E1.cod T.cod +} E T {E.cod = T.cod} T T 1 F {T.cod = T 1.cod T.cod *} T F {T.cod = F.cod} F id {F.cod = id.nome} 60

6.1.3 Código de Três-Endereços Cada instrução faz referência, no máximo, a três variáveis (endereços de memória). As instruções dessa linguagem intermediária são: A = B op C A = op B A = B goto L if A oprel B goto L Exemplo: A = X+Y*Z T1 = Y*Z T2 = X+T1 A = T2 Um código de três-endereços pode ser implementado através de quadruplas (um operador, dois operandos e um resultado) ou triplas (um operador e dois operandos), conforme os exemplos abaixo. Exemplo: A = B*(-C+D) oper arg 1 arg 2 result (0) - C T1 (1) + T1 D T2 (2) * B T2 T3 (3) = T3 A oper arg 1 arg 2 (0) - C (1) + (0) D (2) * B (1) (3) = A (2) Na representação por triplas existentes apontadores para a própria estrutura, evitando assim o uso de temporários. Esquema de Tradução para um comando de atribuição Atrib ID=Expr {Atrib.cod = Expr.cod; Geracod(ID.nome=Expr.nome); } Expr Expr 1 +Expr 2 {Expr.nome = GeraT emp; Expr.cod = Expr 1.cod Expr 2.cod Geracod(Expr.nome=Expr 1.nome+Expr 2.nome); } Expr Expr 1 *Expr 2 {Expr.nome = GeraT emp; Expr.cod = Expr 1.cod Expr 2.cod Geracod(Expr.nome=Expr 1.nome*Expr 2.nome); } Expr (Expr 1 ) {Expr.nome = Expr 1.nome; Expr.cod = Expr 1.cod; } Expr ID {Expr.nome = ID.nome; Expr.cod = } 61

O atributo nome armazena o nome de uma variável (ou temporário). O atributo cod armazena o código fonte gerado para o comando. A função geracod gera um texto correspondente a(s) instrução(ões) fornecida(s) como parâmetro. A função geratemp gera o nome de uma variável temporária. Exemplo para o comando A=X+Y*Z gera o seguinte código: T1 = Y * Z T2 = X + T1 A = T2 Esquema de Tradução para Expressões Lógicas Existem dois métodos principais: representação numérica e representação por fluxo de controle. Representação Numérica Codifica numericamente as constantes true (=1) e false (=0) e avalia o resultado lógico numa variável temporária. Exemplo: Supondo que o código gerado seja armazenado a partir da quadrupla 100 o comando A < B seria traduzido para: 099:... 100: if A < B goto 103 101: T1=0 102: goto 104 103: T1=1 104:... Expr Expr 1 Expr 2 {Expr.nome = GeraT emp; Geracod(Expr.nome = Expr 1.nome Expr 2.nome)} Expr Expr 1 && Expr 2 {Expr.nome = GeraT emp; Geracod(Expr.nome = Expr 1.nome && Expr 2.nome)} Expr! Expr 1 {Expr.nome = GeraT emp; Geracod(Expr.nome =! Expr 1.nome); } Expr (Expr 1 ) {Expr.nome = Expr 1.nome; } Expr ID 1 oprel ID 2 {Expr.nome = GeraT emp; Geracod(if ID 1.nome oprel ID 2.nome goto P roxq + 3); Geracod(Expr.nome =0); Geracod(goto P roxq + 2); Geracod(Expr.nome =1); } Expr true {Expr.nome = GeraT emp; Geracod(Expr.nome=1); } Expr false {Expr.nome = GeraT emp; Geracod(Expr.nome=0); } A variável proxq indica o índice da próxima quádrupla disponível. Exemplo: A < B C < D && E < F 62

100: if A < B goto 103 107: T2=1 101: T1=0 108: if E < F goto 111 102: goto 104 109: T3=0 103: T1=1 110: goto 112 104: if C < D goto 107 111: T3=1 105: T2=0 112: T4=T2 && T3 106: goto 108 113: T5 = T1 T4 O valor final da expressão é sempre no último temporário gerado (no exemplo T5). O nome desse temporário é então armazenado no não-terminal expressão da gramática. Esquema de Tradução para o comando Enquanto O comando < Enquanto > prenquanto < Expr > prfaca < ComSimp >, segue o seguinte esquema de tradução: Através da seguinte regra de tradução: Inicio: Expr.cod if Expr.nome == 0 goto Prox ComSimp.cod goto Inicio Prox:... S prenquanto Expr prfaca {S.inicio = GeraRotulo; S.prox = GeraRotulo; ComSimp S.cod = Expr.cod Geracod(if Expr.nome == 0goto S.prox); ComSimp.cod; Geracod(goto S.inicio); } Representação por Fluxo de Controle Este método traduz expressões lógicas para um código formado por instruções if-goto. São gerados rótulos true e false, que armazenam os endereços de desvio de execução caso a avaliação resulte em true ou false respectivamente. Por este motivo, é mais eficiente que o método de avaliação numérica. Tradução de expressões lógicas por fluxo de controle Expr Expr 1 Expr 2 {Expr 1.true = Expr.true; Expr 1.false = GeraRotulo; Expr 2.true = Expr.true; Expr 2.false = Expr.false; Expr.cod = Expr 1.cod Expr 2.cod; } Expr Expr 1 && Expr 2 {Expr 1.true = GeraRotulo; Expr 1.false = Expr.false; Expr 2.true = Expr.true; Expr 2.false = Expr.false; Expr.cod = Expr 1.cod Expr 2.cod; } Expr! Expr 1 {Expr 1.true = Expr.false; Expr 1.false = Expr.true; Expr.cod = Expr 1.cod; } Expr (Expr 1 ) {Expr 1.true = Expr.true; Expr 1.false = Expr.false; Expr ID 1 oprel ID 2 Expr.cod = Expr 1.cod; } {Expr.cod = Geracod(if ID 1.nome oprel ID 2.nome goto Expr.true); Geracod(goto Expr.f alse); } Expr true {Expr.cod = Geracod(goto Expr.true); } Expr false {Expr.cod = Geracod(goto Expr.f alse); } 63

Exemplo: A < B C < D && E < F L1 L2 if A < B goto RT goto L1 if C < D goto L2 goto RF if E < F goto RT goto RF Supondo que os atributos true e false tenham recebido os rótulos RT e RF. Esquema de Tradução para Comandos de Controle de Fluxo S prse Expr prentao S 1 S prse Expr prentao S 1 prsenao S 2 S prenquanto Expr S 1 {Expr.true = GeraRotulo; Expr.false = S.prox; S 1.prox = S.prox; S.cod = Expr.cod S 1.cod; } {Expr.true = GeraRotulo; Expr.false = GeraRotulo; S 1.prox = S.prox; S 2.prox = S.prox; S.cod = Expr.cod S 1.cod; Geracod(goto S.prox); S 2.cod; } {S.inicio = GeraRotulo; Expr.true = GeraRotulo; Expr.false = S.prox; S 1.prox = S.inicio; S.cod = Expr.cod S 1.cod; Geracod(goto S.inicio); } 6.2 BackPatching (Retrocorreção) O principal problema, na geração de código, é que o código gerado deve incluir comandos de desvio para endereços que, em geral, ainda não são conhecidos. Isso inviabiliza a geração de código num único passo. A solução é utilizar geração de códigos incompletos para os comandos (sem os endereços) que serão devidamente completados quando o endereço destino for conhecido. Chama-se de backpatching ao preenchimento desses endereços não resolvidos. São necessárias três funções para isso: makelist(i) cria uma lista contendo i (um único elemento) e retorna um ponteiro para a lista criada; o elemento i é um índice do vetor de quádruplas; merge(p1,p2) concatena as listas apontadas por p1 e p2, e retorna um ponteiro para lista resultante; backpatching(p,i) insere i (rótulo destino) no campo de endereço de cada uma das quádruplas da lista apontada por p. 64

Backpatching para Expressões Lógicas Foi acrescentado o símbolo não-terminal M, que tem como objetivo guardar o endereço da próxima quádrupla disponível no momento em que M é empilhado (após um && ou ). E E 1 E 2 {backpatching(e 1.ListaF alse, M.quad); E.ListaT rue = merge(e 1.ListaT rue, E 2.ListaT rue); E.ListaF alse = E 2.ListaF alse; } E E 1 && E 2 {backpatching(e 1.ListaT rue, M.quad); E.ListaT rue = E 2.ListaF alse; E.ListaF alse = merge(e 1.ListaF alse, E 2.ListaF alse); } E!E 1 {E.ListaT rue = E 1.ListaF alse; E.ListaF alse = E 1.ListaT rue; } E (E 1 ) {E.ListaT rue = E 1.ListaT rue; E.ListaF alse = E 1.ListaF alse; } E ID 1 oprel ID 2 {E.ListaT rue = makelist(p roxq); E.ListaF alse = makelist(p roxq + 1); Geracod(if ID 1.nome oprel ID 2.nome goto ); Geracod(goto ); } E ID {E.ListaT rue = makelist(p roxq); E.ListaF alse = makelist(p roxq + 1); Geracod(if ID.nome goto ); Geracod(goto ); } M ε {M.quad = P roxq; } Neste esquema, E.ListaTrue e E.ListaFalse são atributos sintetizados (listas) que indicam quádruplas com comandos de desvio incompletos. Os endereços são preenchidos com o valor armazenado em M.quad quando um backpatching ocorre na lista. Exemplo: a < b c < d && e < f E ListaTrue=100,104 ListaFalse=103,105 ListaTrue=100 ListaFalse=101 E M Quad=102 E ListaTrue=104 ListaFalse=103,105 A < B ListaTrue=102 ListaFalse=103 E && M Quad=104 E ListaTrue=104 ListaFalse=105 C < D E < F Figura 6.2: Backpatching para expressões lógicas 100: if A < B goto 100: if A < B goto 101: goto 101: goto 102 102: if C < D goto 102: if C < D goto 104 103: goto 103: goto 104: if E < F goto 104: if E < F goto 105: goto 105: goto 65

Backpatching para Comandos de Controle Entendendo-se o esquema de tradução anterior, é introduzido um não-terminal N antes do comando senao para ocasionar um salto sobre o bloco do senao se for o caso. S Se E entao M S 1 {backpatching(e.listat rue, M.quad); S.prox = merge(e.listaf alse, S 1.prox); } S Se E entao M 1 S 1 N {backpatching(e.listat rue, M 1.quad); senao M 2 S 2 backpatching(e.listaf alse, M 2.quad); S.prox = merge(s 1.prox, merge(n.go, S 2.prox)); } N ε {N.go = makelist(p roxq); Geracod(goto ); } S Enquanto M 1 E Faca {backpatching(s 1.prox, M 1.quad); M 2 S 1 backpatching(e.listat rue, M 2.quad); S.prox = E.ListaF alse; Geracod(goto M 1.quad); } S {L} {S.prox = L.prox; S A {S.prox = makelist(null); } A ID = E Ações semânticas já vistas em aula L L 1 ; M S {backpatching(l 1.prox, M.quad); L.prox = S.prox; } L S {L.prox = S.prox; } P S. {backpatching(s.prox, EOF ); } O endereço indicado por EOF representa o fim do código fonte e o retorno ao sistema operacional. Exercício: Gere o código para o trecho de programa abaixo (supor primeira instrução no endereço 000): Enquanto A < B entao se C < D entao X = Y+Z senao X = Y-Z Código resultante: 000: if A < B goto 002 006: goto 000 001: goto EOF 007: T2 = Y-Z 002: if C < D goto 004 008: X = T2 003: goto 007 009: goto 000 004: T1 = Y+Z EOF: 005: X = T1 66

Capítulo 7 Otimização de Código Trata-se do problema da geração de código eficiente: uso racional da memória e rapidez na execução do código fonte. Porém, muitas vezes esses aspectos são conflitantes, ou seja, apela-se para um maior tempo de execução para se conseguir um ganho no consumo de memória e vice-versa. Compiladores que aplicam transformações de melhorias no código gerado são denominados compiladores otimizantes. Normalmente este processo é feito em duas fases: otimizações do código intermediário e otimizações do código objeto. No código intermediário pode-se eliminar atribuições redundantes, sub-expressões comuns, temporários desnecessários, etc., no intuito de diminui o código intermediário, enquanto que no código objeto é feita uma substituição de instruções por equivalentes mais rápidos que permitem melhor uso dos registradores. 7.1 Otimização Peephole Uma técnica simples para melhorar localmente o código gerado é a otimização peephole, que trabalha substituindo seqüências de instruções (peepholes) por outras mais eficientes. As principais ações da otimização peephole são: eliminação de instruções redundantes otimizações de fluxo de controle simplificações algébricas Eliminação de Instruções Redundantes Uma seqüência de instruções do tipo a = b e b = a, pode ser substituído somente por a = b, pois o resultado lógico é o mesmo. Otimização de Fluxo de Controle Os algoritmos de geração de código intermediário freqüentemente produzem: (1) desvios para desvios, (2) desvios para desvios condicionais ou (3) desvios condicionais para desvios. Uma otimização peephole pode remover tais instruções redundantes: Exemplo 1: 67

goto L1 goto L2...... L1: goto L2 L1: goto L2 Neste caso, se não houverem outras instruções que levem a L1, esta instrução pode ser removida do código. Exemplo 2: if a < b goto L1 if a < b goto L2...... L1: goto L2 L1: goto L2 Exemplo 3: goto L1 if a < b goto L2... goto L3 L1: if a < b goto L2... L3:... L3:... Simplificação Algébrica Remoção de redundâncias que ocorrem em expressões algébricas, tais como: x=x+0 ou y=y*1. 7.2 Otimização de Blocos Sequenciais através de grafos O uso de grafos acíclicos dirigidos (GAD) para se representar uma seqüência de instruções, permite mais facilmente rearranjar a ordem das instruções a fim de reduzir o código objeto final. Exemplo: (a+b)-(e-(c+d)) 68

Código Intermediário T1 = a+b T2 = c+d T3 = e-t2 T4 = T1-T3 T2 = c+d T3 = e-t2 T1 = a+b T4 = T1-T3 Código Objeto MOV a, R0 ADD b, R0 MOV c, R1 ADD d, R1 MOV, R0, T1 MOV E, R0 SUB R1, R0 MOV T1, R1 SUB R0, R1 MOV R1, T4 MOV c, R0 ADD d, R0 MOV e, R1 SUB R0, R1 MOV a, R0 ADD b, R0 SUB R1, R0 MOV R0, T4 7.2.1 Algoritmo para Construir o GAD de um bloco O algoritmo supõe que cada instrução (de três-endereços) segue um dos seguintes três formatos: (1) x = y op z; (2) x = op y; (3) x = y. Instruções if-goto são tratadas como o caso (1). Passos: 1. Se o nó y ainda não existe no grafo, crie uma folha para y (e para z se for o caso 1); 2. No caso 1, verifique se existe um nó op com filhos y e z (nessa ordem). Se sim, chame-o, também de x; senão, crie um nó op com nome x e dois arcos dirigidos do nó op para y e z. No caso 2, verifique se existe um nó op com um único filho y. Se não existir, cria tal nó e um arco para y; chame de x o nó criado ou encontrado. No caso 3, chame também de x o nó y. Exemplo: y = ((a+b)*(a-b))+((a+b)*(a-c)) T1 = a+b T2 = a-b T3 = T1*T2 T4 = a+b T5 = a-c T6 = T4*T5 T7 = T3+T6 y = T7 69

+ T7,y T3 * * T6 + T1,T4 T2 T5 - - a b c Figura 7.1: Grafo Acíclico Dirigido - GAD 7.2.2 Algoritmo para Ordenação de um GAD Cria-se uma lista de ordenação dos nós internos do GAD através do algoritmo a seguir: Enquanto existirem nós interiores n~ao listados faça inicio 1. Selecionar um nó n n~ao listado do qual todos os pais já foram listados; 2. Listar n; Enquanto o filho mais à esquerda m de n tiver todos os pais listados e n~ao for uma folha faça inicio 3. Listar m; 4. n := m fim fim O código é então gerado através da ordem inversa da obtida pela lista resultante. Exemplo para o código intermediário visto anteriormente: y (ou T7), T3, T6, T1 (ou T4), T5, T2. 70

Capítulo 8 Geração de Código Objeto É a fase final de um modelo de compilador. Recebe como entrada a representação intermediária do programa-fonte e produz como saída um programa-alvo equivalente. Por ser impraticável a criação de um gerador de código ótimo, contenta-se com a criação (através de heurísticas) de bons geradores de código. A maioria dos problemas de geração de código envolve aspectos como gerência de memória, seleção de instruções, alocação de registradores e ordem de avaliação. As principais características de um bom gerador de código: deve ser correto (essencial), usar de forma eficiente os recursos da máquina e o próprio gerador deve ser implementado de forma eficiente. Uma entrada usual para um gerador de código é o código intermediário juntamente com as informações armazenadas na tabela de símbolos. Já a saída pode ser uma linguagem absoluta de máquina (carrega em uma posição fixa de memória antes da execução), linguagem relocável (cria módulos que podem ou não, serem executados em conjunto, sendo para tal necessário uma etapa de linkedição dos módulos) ou linguagem de montagem (gera instruções simbólicas que são traduzidas através de um montador). A terceira opção será a escolhida para a implementação do trabalho prático #4. Seleção de Instruções Se não nos importarmos com a eficiência do código-objeto gerado, a seleção de instruções é um processo direto. Para cada tipo de instrução intermediária (código de três-endereços) gerada, projeta-se um esqueleto de código final equivalente. Exemplo: x = y + z. MOV y, R0 (carregar y no registrador R0) ADD z,r0 (adicionar z a R0) MOV R0, x (armazenar R0 em x) Infelizmente, o código final gerado por esta alternativa freqüentemente é extenso e ineficiente. Exemplo: a = b+c d = a+e MOV b, R0 ADD c, R0 MOV R0, a MOV a, R0 ADD e, R0 MOV R0, d 71

Operação com Registradores O uso de registradores no processo de execução de instruções é interessante pois: Operação com registrador é mais rápida Conjunto de registradores é pequeno Utilização eficiente = otimização Desvantagem: a ordem das instruções é muito importante no aspecto eficiência. 8.1 Máquina Objeto Para se criar um gerador de código para uma determinada máquina objeto, primeiramente é necessário conhecer seu conjunto de instruções. Por motivos didáticos optou-se por uma máquina hipotética (MAQHIPO), que tem as seguintes características: É baseada em uma pilha; Trabalha com os mesmos tipos de dados das fases anteriores; Sua memória é dividida em duas partes: Área de Código contém uma lista (CODIGO) das instruções geradas pelo compilador (código de três-endereços); Área de Dados composta por uma pilha (DADOS) que conterá os registradores manipulados pelas instruções da máquina (só existe em tempo de execução de um programa) e uma lista (MEMO) que armazena as variáveis e constantes manipuladas pelo programa. Para efeito de simplificação, todas as variáveis terão tamanho 1; Possui um registrador especial (PROXINST) para a próxima instrução (na área de código) a ser executada, outro (TOPODADOS) que indica o topo da pilha de dados e TOPOMEMO a quantidade de memória alocada; O funcionamento da MAQHIPO é bastante simples: 1. Carrega-se o programa-objeto (gerado pelo compilador) na área de código. 2. Todas as instruções (indicadas pelo ponteiro PROXINST) são executadas seqüencialmente até a instrução de parada ou até que ocorra algum erro de execução; 3. A execução de cada instrução incrementa o valor de PROXINST (exceto para instruções de desvio); O conjunto de instruções válidas para a MAQHIPO é: Inicialização e Finalização 72

INIP inicializa a execução do programa {T opomemo = T opodesvio = 0; T opodados = 1; } FIMP encerra a execução do programa {} Alocação de Memória ALME m t Aloca m posições de memória, todas do tipo t {for(i = T opomemo; i < T opomemo + m; i + +)Memo[i].tipo = t; T opomemo+ = m; } DEAL m Desloca m posições de memória {T opomemo = m; } Comandos de E/S ENTR Entrada de dados {gets(dados[+ + T opodados]); } IMPR Impressão de valores {printf( %s, Dados[T opodados ]); } Comandos para Expressões CRCT k t Carrega uma constante do tipo t. {Dados[+ + T opodados] = (k, t); } CRVL n t Transporta o conteúdo do endereço de memória n para a pilha de dados. {Dados[+ + T opodados] = Memo[n]; } SOMA substitui os dois elementos mais ao topo da pilha por sua soma. {Dados[T opodados 1]+ = Dados[T opodados ]; } SUBT substitui os dois elementos mais ao topo da pilha por sua diferença. {Dados[T opodados 1] = Dados[T opodados ]; } MULT substitui os dois elementos mais ao topo da pilha por seu produto. {Dados[T opodados 1] = Dados[T opodados ]; } DIVI substitui os dois elementos mais ao topo da pilha por seu quociente. {Dados[T opodados 1]/ = Dados[T opodados ]; } POTE substitui os dois elementos mais ao topo da pilha pela sua potência. {Dados[T opodados 1] = pow(dados[t opodados 1], Dados[T opodados ]); } INVE inverte o sinal do elemento no topo da pilha. {Dados[T opodados] = 1; } CONJ substitui os dois elementos mais ao topo da pilha por sua conjunção (&&). {Dados[T opodados 1] = Dados[T opodados 1] && Dados[T opodados]; T opodados ; } DISJ substitui os dois elementos mais ao topo da pilha por sua disjunção ( ). {Dados[T opodados 1] = Dados[T opodados 1] + Dados[T opodados] == 1; T opodados ; } 73

DISX substitui os dois elementos mais ao topo da pilha por sua disjunção exclusiva (& ). {Dados[T opodados 1] = Dados[T opodados 1] & Dados[T opodados]; T opodados ; } NEGA inverte o valor lógico do elemento no topo da pilha. {Dados[T opodados] =!Dados[T opodados]; } CPME substitui os dois elementos mais ao topo da pilha pela comparação de menor. {Dados[T opodados 1] = Dados[T opodados 1] < Dados[T opodados]; T opodados ; } CMAI substitui os dois elementos mais ao topo da pilha pela comparação de maior. {Dados[T opodados 1] = Dados[T opodados 1] > Dados[T opodados]; T opodados ; } CPIG substitui os dois elementos mais ao topo da pilha pela comparação de igualdade. {Dados[T opodados 1] = Dados[T opodados 1] == Dados[T opodados]; T opodados ; } CDIF substitui os dois elementos mais ao topo da pilha pela comparação de desigualdade. {Dados[T opodados 1] = Dados[T opodados 1]! = Dados[T opodados]; T opodados ; } CPMI substitui os dois elementos mais ao topo da pilha pela comparação de menor ou igual. {Dados[T opodados 1] = Dados[T opodados 1] <= Dados[T opodados]; T opodados ; } CPMA substitui os dois elementos mais ao topo da pilha pela comparação de maior ou igual. {Dados[T opodados 1] = Dados[T opodados 1] >= Dados[T opodados]; T opodados ; } 74

Comando de Atribuição ARMZ n Transporta o conteúdo do topo da pilha para o endereço de memória n. {Memo[n] = Dados[T opodados ]; } Comandos Condicionais e Iterativos DSVF p Desvio se condicional falso para a instrução p. {P roxinst = Dados[T opodados]?p roxinst + 1 : p; } DSVI p Desvio incondicional para a instrução p. {P roxinst = p; } EXEC n Executa a chamada a uma sub-rotina com inicio na instrução n e empilha o endereço de retorno {Desvios[+ + T opodesvio] = P roxinst + 1; P roxinst = n; } RETR Retorna a execução para a próxima instrução após a chamada à sub-rotina {P roxinst = Desvios[T opodesvio ]; } Exemplo de Geração de Código Objeto na Linguagem Hipo: Programa Fonte Programa Objeto Var: inteiro F, N; 01 INIP 02 ALME 1 i 03 ALME 1 i Inicio Leia(N); 04 ENTR 05 ARMZ 1 F=1; 06 CRCT 1 i 07 ARMZ 0 Enquanto N >= 1 faca Inicio 08 CRVL 1 i 09 CRCT 1 i 10 CPMA 11 DSVF 21 F = N; 12 CRVL 0 i 13 CRVL 1 i 14 MULT 15 ARMZ 0 N ; 16 CRVL 1 i 17 CRCT 1 i 18 SUBT 19 ARMZ 1 Fim 20 DSVI 8 Escreva(F); 21 CRVL 0 i 22 IMPR Fim 23 FIMP 75

8.1.1 Regras para Geração de Código Objeto Programa AreaDecl AreaSubRot Principal { Geracod(INIP); Gerar código para AreaDecl Quad = ProxQ; Geracod(DSVI); Gerar código para AreaSubRot Backpatching(Quad, ProxQ); Gerar código para Principal if(ts.qtde 0) Geracod(DEAL TS.Qtde); Geracod(FIMP);} AreaDecl AreaDeclVar AreaDecl { Gerar código para AreaDeclVar Gerar código para AreaDecl } AreaDecl AreaDeclConst AreaDecl { Gerar código para AreaDeclConst Gerar código para AreaDecl } AreaDecl ε {} AreaDeclVar prvar DoisPt DeclVars { Gerar código para DeclVars } DeclVars Tipo ListaID PtVirg DeclVars { ContIds=0; Gerar código para ListaID Gerar código para DeclVars } DeclVars Tipo ListaID PtVirg DeclVars { ContIds=0; Gerar código para ListaID Gerar código para DeclVars } DeclVars ε {} Tipo print {} Tipo prfloat {} Tipo prchar {} Tipo prstring {} Tipo prbool {} 76

ListaID Identificador ListaID { if(acao == DeclV ar)contids + +; else{geracod(en T REntrada.T ipo); Geracod(ARM Z Entrada.Endereco Entrada.T ipo); } Gerar código para ListaID } ListaID Virg Identificador ListaID { if(acao == DeclV ar)contids + +; else{geracod(en T REntrada.T ipo); Geracod(ARM Z Entrada.Endereco Entrada.T ipo); } Gerar código para ListaID } ListaID ε { if(acao == DeclV ar)geracod(alme ContIds); } AreaDeclConst prconst DoisPt ListaConst { Gerar código para ListaConst } ListaConst Tipo ListaIDConst PtVirg ListaConst { Gerar código para ListaIDConst Gerar código para ListaConst } ListaConst Tipo ListaIDConst PtVirg ListaConst { Gerar código para ListaIDConst Gerar código para ListaConst } ListaConst ε {} ListaIDConst Identificador Atrib Valor ListaIDConst { Geracod(ALME 1 Identificador.tipo); Gerar código para Valor Geracod(ARMZ Identificador.Endereco); Gerar código para ListaIDConst } ListaIDConst Virg Identificador Atrib Valor ListaIDConst { Geracod(ALME 1 Identificador.tipo); Gerar código para Valor Geracod(ARMZ Identificador.Endereco); Gerar código para ListaIDConst } ListaIDConst ε {} Valor OpAritSubt Numeros { Gerar código para Numeros Geracod(INVE); } Valor Numeros { Gerar código para Numeros } 77

Valor ConstCaracter { Geracod(CRCT ConstCaracter.lexema c); } Valor ConstString { Geracod(CRCT ConstString.lexema s); } Valor prtrue { Geracod(CRCT true b); } Valor prfalse { Geracod(CRCT False b); } Numeros NumeroInteiro { Geracod(CRCT NumeroInteiro.lexema i); } Numeros NumeroReal { Geracod(CRCT NumeroReal.lexema f); } AreaSubRot AreaProc AreaSubRot { Gerar código para AreaProc Gerar código para AreaSubRot } AreaSubRot AreaFunc AreaSubRot { Gerar código para AreaFunc Gerar código para AreaSubRot } AreaSubRot ε { } AreaProc prproc IdentSR AbrePar ListaParam FechaPar AreaDecl BlocoCom { Gerar código para ListaParam Gerar código para AreaDecl Gerar código para BlocoCom if(ts.qtde 0) Geracod(DEAL TS.qtde); Geracod(RETR); } ListaParam Tipo Identificador ListaParam { Geracod(ARMZ Identificador.endereco); Gerar código para ListaParam } ListaParam ε { } ListaParam Virg Tipo Identificador ListaParam { Geracod(ARMZ Identificador.endereco); Gerar código para ListaParam } 78

ListaParam ε { } AreaFunc prfunc Tipo IdentSR AbrePar ListaParam FechaPar AreaDecl BlocoCom { Gerar código para ListaParam Gerar código para AreaDecl Gerar código para BlocoCom if(ts.qtde 0) Geracod(DEAL TS.qtde); Geracod(RETR); } Principal prmain Abrepar FechPar BlocoCom { Gerar código para BlocoCom } BlocoCom AbreChaves ListaCom FechaChaves { Gerar código para ListaCom } ListaCom Comando ListaCom { Gerar código para Comando Gerar código para ListaCom } ListaCom ε {} Comando Atrib PtVirg { Gerar código para Atrib } Comando RepetPre { Gerar código para RepetPre } Comando RepetPos PtVirg { Gerar código para RepetPos } Comando RepetCont { Gerar código para RepetCont } Comando Entrada PtVirg { Gerar código para Entrada } Comando Saida PtVirg { Gerar código para Saida } Comando Condic { Gerar código para Condic } Comando BlocoCom { Gerar código para BlocoCom } 79

Comando Retorno PtVirg { Gerar código para Retorno } Comando SubRot PtVirg { Gerar código para SubRot } Atrib Identificador Atrib { Gerar código para Atrib ; Geracod(ARMZ Identificador.Endereco) } Atrib SimbAtrib Expr { Gerar código para Expr } Atrib SimbAtribSoma Expr { Geracod(CRVL Entrada.Endereco Entrada.tipo) Gerar código para Expr Geracod(SOMA) } Atrib SimbAtribSubt Expr { Geracod(CRVL Entrada.Endereco Entrada.tipo) Gerar código para Expr Geracod(SUBT) } Atrib SimbAtribMult Expr { Geracod(CRVL Entrada.Endereco Entrada.tipo) Gerar código para Expr Geracod(MULT) } Atrib SimbAtribDivi Expr { Geracod(CRVL Entrada.Endereco Entrada.tipo) Gerar código para Expr Geracod(DIVI) } Atrib SimbIncr { Geracod(CRVL Entrada.Endereco Entrada.tipo) Geracod(CRCT 1 i) Geracod(SOMA) } Atrib SimbDecr { Geracod(CRVL Entrada.Endereco Entrada.tipo) Geracod(CRCT 1 i) Geracod(SUBT) } Condic prif AbrePar Expr FechPar Comando Condic { Gerar código para Expr; Quad1 = Proxq Geracod(DSVF); Gerar código para Comando Gerar código para Condic } 80

Condic prelse Comando { Quad2 = ProxQ; Geracod(DSVI) Backpatching(Quad1, ProxQ); Gerar código para Comando Backpathing(Quad2, ProxQ); } Condic ε { Backpatching(Quad1, ProxQ) } RepetPos prdo BlocoCom prwhile AbrePar Expr FechaPar { Quad1 = ProxQ; Gerar código para BlocoCom Gerar código para Expr; Geracod(DSVF ProxQ+2); Geracod(DSVI Quad1) } RepetPre prwhile AbrePar ExprFechaPar Comando { Quad1 = ProxQ; Gerar código para Expr Quad2 = ProxQ; Geracod(DSVF); Gerar código para Comando Geracod(DSVI Quad1); Backpatching(Quad2, ProxQ) } RepetCont prfor AbrePar Atrib PtVirg Expr PtVirg Atrib FechaPar Comando { Gerar código para Atrib; Quad1 = ProxQ; Gerar código para Expr; Quad2 = ProxQ Geracod(DSVF); Buffer = Gerar código para Atrib Gerar código para Comando Descarrega o buffer temporário; Geracod(DSVI Quad1) Backpatching(Quad2, ProxQ) } Entrada prscanf AbrePar ListaID FechaPar { Gerar código para ListaID } Saida prprint Abrepar ListaExpr FechaPar { Gerar código para ListaExpr } Saida prprintl Abrepar ListaExpr FechaPar { Gerar código para ListaExpr } ListaExpr Expr ListaExpr { Gerar código para Expr; Gerar código para ListaExpr } ListaExpr VirgExpr ListaExpr { Gerar código para Expr; Gerar código para ListaExpr } ListaExpr ε{} Retorno prreturn Expr { Gerar código para Expr } 81

SubRot IdentSR AbrePar ListaArg FechaPar { Gerar código para ListaArg; Geracod(EXEC IdentSR.Endereco) } ListaArg ListaExpr { Gerar código para ListaExpr } ListaArg ε{} Expr TermoLog Expr Ternario { Gerar código para TermoLog; Gerar código para Expr Gerar código para Ternario } Ternario Interrog Expr 1 DoisPt Expr 2 { Quad1 = ProxQ; Geracod(DSVF); Gerar código para Expr 1 Quad2 = ProxQ; Geracod(DSVI); Backpatching(Quad1, ProxQ) Gerar código para Expr 2 ; Backpatching(Quad2, ProxQ) } Ternario ε{} Expr OpLogAnd TermoLog Expr { Gerar código para TermoLog; Geracod(CONJ) Gerar código para Expr } Expr OpLogOr TermoLog Expr { Gerar código para TermoLog; Geracod(DISJ) Gerar código para Expr } Expr OpLogXor TermoLog Expr { Gerar código para TermoLog; Geracod(DISX) Gerar código para Expr } Expr ε {} TermoLog FatorLog TermoLog { Gerar código para FatorLog; Gerar código para TermoLog ; } TermoLog OpRelacMaior FatorLog { Gerar código para FatorLog; Geracod(CMAI); } TermoLog OpRelacMenor FatorLog { Gerar código para FatorLog; Geracod(CPME); } TermoLog OpRelacMenorIgual FatorLog { Gerar código para FatorLog; Geracod(CPMI); } 82

TermoLog OpRelacMaiorIgual FatorLog { Gerar código para FatorLog; Geracod(CPMA); } TermoLog OpRelacIgual FatorLog { Gerar código para FatorLog; Geracod(CPIG); } TermoLog OpRelacDifer FatorLog { Gerar código para FatorLog; Geracod(CDIF); } TermoLog ε{} FatorLog ExprAr { Gerar código para ExprAr; } FatorLog OpLogNeg Expr { Gerar código para Expr; Geracod(NEGA); } FatorLog prtrue { Geracod(CRCT true l); } FatorLog prfalse { Geracod(CRCT false l); } ExprAr TermoAr ExprAr { Gerar código para TermoAr; Gerar código para ExprAr ; } ExprAr OpAdic TermoAr ExprAr { Gerar código para TermoAr; Geracod(SOMA); Gerar código para ExprAr ; } ExprAr OpSubt TermoAr ExprAr { Gerar código para TermoAr; Geracod(SUBT); Gerar código para ExprAr ; } ExprAr ε{} TermoAr FatorAr TermoAr { Gerar código para FatorAr; Gerar código para TermoAr ; } TermoAr OpMult FatorAr TermoAr { Gerar código para FatorAr; Geracod(MULT); Gerar código para TermoAr ; } TermoAr OpDivi FatorAr TermoAr { Gerar código para FatorAr; Geracod(DIVI); Gerar código para TermoAr ; } 83

TermoAr ε{} FatorAr ElementoAr FatorAr { Gerar código para ElementoAr; Gerar código para FatorAr ; } FatorAr OpPote ElementoAr FatorAr { Gerar código para ElementoAr; Geracod(POTE); Gerar código para FatorAr ; } FatorAr ε{} ElementoAr AbrePar Expr FechaPar { Gerar código para Expr; } ElementoAr OpSubt ExprAr { Gerar código para ExprAr; Geracod(INVE); } ElementoAr Identificador { Geracod(CRVL identificador.endereco); } ElementoAr Numeros { Gerar código para Numeros } ElementoAr SubRot { Gerar código para SubRot } ElementoAr ConstCaracter { Geracod(CRCT ConstCaracter.lexema c); } ElementoAr ConstString { Geracod(CRCT ConstString.lexema s); } 8.1.2 Trabalho Prático #4 Implementar um módulo analisador semântico para um protótipo de compilador para a linguagem P ASCAL jr (simplificada) vista em aula. Características: Do módulo gerador de código: Cada chamada a elementos não-terminais processa esquemas de tradução de código necessários para cada produção da gramática. Utiliza a árvore decorada criada no módulo semântico. Utiliza a tabela de símbolos (altera e consulta dados) construída na fase anterior. 84

Do programa a ser criado: Abre um arquivo fonte para análise. Executa todas as fases de análise e, caso nenhum erro seja encontrado, armazena a sequência de instruções em linguagem HIPO na lista CODE da MaqHipo. 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. Inicia o processo de execução da máquina objeto, segundo as regras definidas para cada instrução. Pára o processo de execução ao atingir a instrução FIMP (neste caso informa fim de execução) ou algum erro de execução seja detectado (neste caso informa mensagem de erro). Critérios de Avaliação: Implementação usando linguagem C ou C++. Entrega de fontes e executável (em um disquete identificado ou e-mail). Grupo de 02 alunos (máximo). Valor do trabalho: 10.0 (25% da nota prática). Data de Entrega: 30/06/2005. Punições: de 20% do valor do trabalho por dia de atraso. de 1.75 por erro léxico não analisado corretamente. de 1.5 por erro sintático não analisado corretamente. de 1.25 por erro semântico não analisado corretamente. de 1.0 por erro semântico não analisado corretamente. de 0.5 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). 85