• Nenhum resultado encontrado

Escreva uma gramática para expressões que possam incluir operadores binários

Codificação direta da tabela

13. Escreva uma gramática para expressões que possam incluir operadores binários

(+ e ×), menos unário (-), autoincremento (++) e autodecremento (--) com sua precedência normal. Suponha que os menos unários repetidos não sejam per- mitidos, mas sim os operadores de autoincremento e autodecremento repetidos.

associar uma ação dirigida pela sintaxe (ver Capítulo 4) com a produção inútil. Como seu algoritmo de construção de tabela modificado deverá lidar com uma ação associada a uma produção inútil?

141

Capítulo

VISÃO GERAL DO CAPÍTULO

Um programa de entrada gramaticalmente correto ainda pode conter sérios erros que impediriam a compilação. Para detectá-los, um compilador realiza outro nível de verificação que envolve a consideração de cada comando em seu contexto real. Essas verificações encontram erros de tipo e de concordância.

Este capítulo introduz duas técnicas para a verificação sensível ao contexto. As gramá- ticas de atributo são um formalismo funcional para especificar a computação sensível ao contexto. A tradução ad hoc dirigida pela sintaxe oferece um framework simples no qual o construtor de compiladores pode pendurar trechos de código quaisquer para realizar essas verificações.

Palavras-chave: Elaboração semântica, Verificação de tipo, Gramáticas de atributo,

Tradução ad hoc dirigida pela sintaxe

4.1 INTRODUÇÃO

A tarefa final do compilador é traduzir o programa de entrada para um formato que possa ser executado diretamente na máquina-alvo. Para esta finalidade, ele precisa de conhecimento sobre o programa de entrada que vai bem além da sintaxe. O compilador precisa acumular uma grande base de conhecimento sobre a computação detalhada que está codificada no programa de entrada; saber quais valores são representados, onde residem e como fluem de um nome para outro; entender a estrutura da computação e analisar como o programa interage com arquivos e dispositivos externos. Tudo isto pode ser derivado do código-fonte, usando conhecimento contextual. Assim, o compilador precisa realizar uma análise mais profunda, que é típica para um scanner ou um parser. Esses tipos de análise são, ou realizados ao longo da análise sintática, ou em um passo posterior que percorre a IR produzida pelo parser. Chamamos esta análise de “análise sensível ao contexto”, para diferenciá-la da análise sintática, ou de “elaboração semân- tica”, pois elabora a IR. Este capítulo explora duas técnicas para organizar este tipo de análise em um compilador: uma abordagem automatizada com base nas gramáticas de atributo, e uma abordagem ad hoc, que se baseia em conceitos semelhantes.

Roteiro conceitual

Para acumular o conhecimento contextual necessário para tradução adicional, o com- pilador precisa desenvolver meios para visualizar o programa que não sejam pela sintaxe. Ele usa abstrações que representam algum aspecto do código, como um sis- tema de tipos, um mapa de armazenamento ou um grafo de fluxo de controle. Ele precisa entender o espaço de nomes do programa: os tipos de dados representados no programa, os tipos de dados que podem ser associados a cada nome e cada expressão, e o mapeamento desde o aparecimento de um nome no código até a ocorrência es- pecífica desse nome; e entender o fluxo de controle, tanto dentro dos procedimentos quanto entre os procedimentos. O compilador terá uma abstração para cada uma dessas categorias de conhecimento.

x. Antes que o compilador possa emitir um código executável na máquina-alvo para as computações envolvendo x, ele precisa ter respostas para muitas perguntas.

j Que tipo de valor está armazenado em x? As linguagens de programação

modernas usam diversos tipos de dados, incluindo números, caracteres, valores booleanos, ponteiros para outros objetos, conjuntos (como {red, yellow, green}) e outros. A maioria das linguagens inclui objetos compostos que agre- gam valores individuais; estes incluem arrays, estruturas, conjuntos e strings.

j Qual é o tamanho de x? Como o compilador precisa manipular x, ele precisa

saber o tamanho da representação de x na máquina-alvo. Se x for um número, ele poderia ocupar uma palavra (um inteiro ou número de ponto flutuante), duas palavras (um número de ponto flutuante de precisão dupla ou um número complexo) ou quatro palavras (um número de ponto flutuante com precisão quádrupla ou um número complexo com precisão dupla). Para arrays e strings, o número de elementos poderia ser fixado em tempo de compilação ou ser determinado em tempo de execução (runtime).

j Se x é um procedimento, que argumentos ele utiliza? Que tipo de valor, se

houver, ele retorna? Antes que o compilador possa gerar código para chamar um procedimento, precisa saber quantos argumentos o código para o procedimento chamado espera, onde imagina encontrar esses argumentos e que tipo de valor espera de cada argumento. Se o procedimento retorna um valor, onde a rotina que chama o encontrará, e que tipo de dado ele será? (O compilador precisa garantir que o procedimento que chama, use o valor de maneira coerente e segura. Se o procedimento que chama, pressupõe que o valor de retorno é um ponteiro que ele pode seguir para encontrar o valor apontado, e o procedimento chamado retornar uma string de caracteres qualquer, os resultados podem não ser previsíveis, seguros ou coerentes.)

j Por quanto tempo o valor de x deve ser preservado? O compilador precisa

garantir que o valor de x permanece acessível para qualquer parte da computação que possa legalmente referenciá-lo. Se x for uma variável local em Pascal, o compilador pode facilmente superestimar seu tempo de vida preservando seu valor pela duração do procedimento que declara x. Se x for uma variável global que possa ser referenciada em qualquer lugar, ou um elemento de uma estrutura alocada explicitamente pelo programa, o compilador pode ter certa dificuldade para determinar seu tempo de vida. O compilador sempre poderá preservar o valor de x por toda a computação; porém, informações mais precisas sobre o tempo de vida de x poderiam permitir que o compilador reutilizasse seu espaço para outros valores com tempos de vida não conflitantes.

j Quem é responsável por alocar espaço para x (e inicializá-lo)? O espaço

para ele? Se a alocação é explícita, então o compilador precisa assumir que o endereço de x não pode ser conhecido até que o programa seja executado. Se, por outro lado, o compilador alocar espaço para x em uma das estruturas de dados que ele gerencia, então ele sabe mais sobre o endereço de x. Esse conhecimento pode permitir que ele gere um código mais eficiente.

O compilador precisa obter as respostas para essas perguntas, e de outras, a partir do programa fonte e das regras da linguagem-fonte. Em uma linguagem tipo Algol, como Pascal ou C, a maior parte dessas perguntas pode ser respondida examinando-se as de- clarações para x. Se a linguagem não possui declarações, como em APL, o compilador precisa obter esse tipo de informação analisando o programa ou deve gerar código que possa tratar qualquer caso que possa surgir.

Muitas dessas perguntas (se não todas) estão fora da sintaxe livre de contexto da linguagem-fonte. Por exemplo, as árvores sintáticas para x ← y e x ← z diferem apenas no texto do nome no lado direito da atribuição. Se x e y são inteiros, enquanto z é uma string de caracteres, o compilador pode ter que emitir um código diferente para x ← y do que emite para x ← z. Para distinguir entre esses casos, ele precisa se aprofundar no significado do programa. As análises léxica e sintática lidam unicamente com a forma do programa; a análise de significado está no âmbito da análise sensível ao contexto. Para ver essa diferença entre sintaxe e significado mais claramente, considere a estrutura de um programa na maioria das linguagens tipo Algol. Essas linguagens exigem que cada variável seja declarada antes de ser usada e que cada uso de uma variável seja coerente com sua declaração. O construtor de compiladores pode estruturar a sintaxe para garantir que todas as declarações ocorram antes de qualquer comando executável. Uma produção como

na qual os não terminais possuem os significados óbvios, garante que todas as decla- rações ocorrerão antes de quaisquer comandos executáveis. Esta restrição sintática não faz nada para verificar a regra mais profunda — que o programa realmente declare cada variável antes do seu primeiro uso em um comando executável. E também não fornece um modo óbvio de lidar com a regra em C + + que exige a declaração antes do uso para algumas categorias de variáveis, mas permite que o programador misture declarações e comandos executáveis.

Impor a regra “declarar antes de usar” exige um nível de conhecimento mais profundo do que pode ser codificado na gramática livre de contexto. Esta gramática lida com categorias sintáticas ao invés de palavras específicas. Assim, ela pode especificar as posições em uma expressão na qual um nome de variável pode ocorrer. O parser pode reconhecer que a gramática permite que um nome de variável ocorra, e dizer que ele ocorreu. Porém, a gramática não tem como corresponder uma ocorrência de um nome de variável com outra; isto exigiria que a gramática especificasse um nível de análise muito mais profundo — uma análise que possa considerar o contexto e possa examinar e manipular informações em um nível mais profundo do que a sintaxe livre de contexto.

4.2 INTRODUÇÃO AOS SISTEMAS DE TIPO

A maior parte das linguagens de programação associa uma coleção de propriedades a cada valor de dados. Chamamos esta coleção de propriedades de tipo do valor. O tipo especifica um conjunto de propriedades mantidas em comum por todos os valores desse tipo. Os tipos podem ser especificados por uma condição de pertinência; por

Para resolver esse problema, o compilador normalmen- te cria uma tabela de nomes. Ele insere um nome na declaração; e pesquisa o nome a cada referência. Uma falha na pesquisa indica a falta de uma declaração. Essa solução ad hoc exige muito do parser, mas utiliza mecanismos que estão bem fora do escopo das linguagens livres de contexto.

4.2.1 A finalidade dos sistemas de tipos

Os projetistas de linguagens de programação introduzem sistemas de tipos de modo que possam especificar o comportamento do programa em um nível mais preciso do que é possível em uma gramática livre de contexto. O sistema de tipos cria um segundo vocabulário para descrever a forma e o comportamento dos programas válidos. A análise de um programa do ponto de vista do seu sistema de tipos gera informações que não podem ser obtidas usando as técnicas de análises léxica e sintática. Em um compilador, essa informação normalmente é usada para três finalidades distintas: segurança, expressividade e eficiência de execução.