Buscar

Construção de compiladores

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes
Você viu 3, do total de 34 páginas

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes
Você viu 6, do total de 34 páginas

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes
Você viu 9, do total de 34 páginas

Faça como milhares de estudantes: teste grátis o Passei Direto

Esse e outros conteúdos desbloqueados

16 milhões de materiais de várias disciplinas

Impressão de materiais

Agora você pode testar o

Passei Direto grátis

Você também pode ser Premium ajudando estudantes

Prévia do material em texto

PDF gerado usando o pacote de ferramentas em código aberto mwlib. Veja http://code.pediapress.com/ para mais informações.
PDF generated at: Fri, 06 Dec 2013 10:27:13 UTC
Construção de compiladores
Conteúdo
Páginas
Capa 1
O que é um compilador 1
Fases de um compilador 2
Teoria das linguagens formais 2
Linguagens regulares e autômatos finitos 3
Linguagens livres de contexto 4
Análise léxica 6
Análise sintática 10
Tradução dirigida por sintaxe 18
Análise semântica 19
Geração de código intermediário 21
Otimização de código 26
Nomes dos colaboradores 29
Referências
Fontes e Editores da Página 30
Fontes, Licenças e Editores da Imagem 31
Licenças das páginas
Licença 32
Capa 1
Capa
. . Construção de compiladores
Ir para o índice
O que é um compilador
Um compilador é um tipo de tradutor que lê um programa escrito numa linguagem, a linguagem fonte, e
transforma-o em um outro programa equivalente escrito em outra linguagem, a linguagem objeto.
Existem outros tipos de tradutores sendo um deles o interpretador que, obtém um programa escrito em alguma
linguagem e o converte para código executável. A principal diferença entre um compilador e um interpretador é que
os compiladores geram códigos que podem ser executados diretamente pelo computador, enquanto que, no
interpretador o programa fonte precisa ser traduzido toda vez que for executado. Linguagem como Pascal, COBOL,
C e C++ são compiladas enquanto que linguagens como PHP, Perl, Javascript e Python são interpretadas.
Abaixo um exemplo de um programa em linguagem de alto nível e seu equivalente em linguagem de baixo nível, ou
seja, depois do processo de compilação.
Linguagem de alto nível
c = a + b;
Linguagem de baixo nível
mov ax, $a
add ax, $b
mov $c, ax
É importante notar que o programa fonte e o programa objeto são completamente diferentes, porém possuem o
mesmo significado. Além da tradução é importante destacar a tarefa do compilador na detecção de erros feitos pelo
programador, caso isso ocorra o programa objeto não é gerado e uma ou mais mensagens de erro são exibidas.
Fases de um compilador 2
Fases de um compilador
Um compilador possui várias fases que podem ser divididas em dois grupos, análise e síntese.
A análise é constituída geralmente de três fases que são, análise léxica, análise sintática e análise semântica,
enquanto que a fase de síntese é composta por módulos de geração e otimização de código.
Esta página é um esboço de informática. Ampliando-a você ajudará a melhorar o Wikilivros.
Teoria das linguagens formais
Teoria das Linguagens Formais e dos autômatos é o estudo de modelos matemáticos que possibilitam a
especificação e o reconhecimento de linguagens (no sentido amplo da palavra), suas classificações, estruturas,
propriedades, características e inter-relacionamentos.
Uma linguagem formal é um conjunto de palavras,isto é, um conjunto finito de letras ou símbolos com um conjunto
de regras para combinar estes elementos. O inventário destas letras é chamado de alfabeto na qual esta linguagem é
definida. O modo formal como uma linguagem é definida é chamada de gramática formal.
Assim, podemos dizer que "Linguagens formais" são mecanismos formais para representação e especificação de
linguagens, baseados na chamada "Teoria da Computação". As representações podem ser feitas por reconhecedores e
geradores. Os reconhecedores são dispositivos formais que servem para verificar se uma sentença pertence ou não à
determinada linguagem. São os autômatos: autômatos finitos, autômatos de pilha e Máquina de Turing. Os sistemas
geradores são dispositivos formais que permitem a geração sistemática de todas as sentenças de uma linguagem. Os
principais sistemas geradores disponíveis são as gramáticas, onde se destacam as gramáticas de Chomsky. Então,
linguagens formais podem ser representadas de maneira finita e precisa através de sistemas com sustentação
matemática.
Importância na Computação
A importância dessa teoria na Ciência da Computação é dupla: ela tanto apóia outros aspectos teóricos da Ciência da
Computação (Decibilidade, computabilidade, complexidade computacional, por exemplo), como fundamenta
diversas aplicações computacionais tais como processamento de linguagens, reconhecimento de padrões, modelagem
de sistemas.
Linguagens regulares e autômatos finitos 3
Linguagens regulares e autômatos finitos
A linguagem regular pode ser descrita por [1][2][3]:
•• Autômatos finitos
•• Gramáticas regulares
•• Expressões regulares
Autômatos Finitos
Paradigma reconhecedor. Descrevem uma expressão regular. Podem ser classificados em:
Autômato Finito Determinístico: Representado pela quintupla ( ,Q, ,q,F) que consite em:
• : Alfabeto - Conjunto finito de símbolos de entrada
• Q: Conjunto finito de estados
• : Função de transição ( : Q x Q)
• q: Estado inicial
• F: Conjunto de estados finais
Um autômato finito determinístico aceita uma cadeia de símbolos se após um estado inicial, mudando de estados por
meio da função (que determina precisamente o próximo estado), consegue chegar à um estado final.
Regras:
•• Todo estado deve conter uma e somente uma transição para cada símbolo do alfabeto.
Autômato Finito Não-Determinístico: Representado pela quintupla ( ,Q, ,q,F), onde a função de transição 
pode conduzir à múltiplos estados ou até mesmo nenhum.
Regras:
•• Não é necessário uma transição para cada símbolo do alfabeto;
•• Podem haver várias transições partindo de um mesmo estado na leitura de um mesmo símbolo;
•• Não é necessário mapear todas as possibilidades, mas, somente o que valida a cadeia.
Gramáticas Regulares
Conhecida também como Tipo 3 da Hierarquia de Chomsky. Pode ser definida como gramática linear.
As gramáticas regulares caracterizam as linguagens regulares:
•• Toda linguagem gerada por uma gramática regular é regular.
•• Toda linguagem regular pode ser caracterizada por uma gramática linear a direita, e por uma gramática linear a
esquerda.
Teorema 1: Se a linguagem L pode ser gerada por uma gramática regular, então L é regular.
Teorema 2: Se a linguagem L é regular, então L pode ser gerada por uma gramática linear a esquerda e por uma
gramática linear a direita.
Linguagens regulares e autômatos finitos 4
Expressões Regulares
Definição:
Trata-se de um paradigma gerador. Um modelo que denota qualquer linguagem regular de forma mais simples,
verificando o conjunto de cadeias de símbolos permitidos na linguagem.
Aplicação:
•• Sistemas de pesquisa de texto, geradores de analisadores léxicos.
•• Algoritmos de reconhecimento de pouca complexidade, grande eficiência e de fácil implementação.
•• Sistemas operacionais, protocolos, editores, compiladores.
Referências
[1] Roberto Claudino da Silva - http:/ / www. claudino. unifei. edu. br/
[2] José Lucas Rangel - http:/ / www. inf. puc-rio. br/ ~inf1302/
[3] http:/ / teia. inf. ufrgs. br/ cgi-bin/ moore. pl?curso=LivroAnimado& estado=281
Linguagens livres de contexto
Na teoria de linguagens formais, uma linguagem livre de contexto é uma linguagem gerada por alguma gramática
livre de contexto. O conjunto de todas as linguagens livres de contexto é idêntico ao conjunto de linguagens aceitas
por um autômato de pilha. De acordo com a Hierarquia de Chomsky, linguagens livres de contexto são Tipo-2.
As gramáticas livres de contexto são da seguinte forma:
V -> w
de modo que V é um símbolo não-terminal e w é uma cadeia composta de terminais e/ou de não-terminais. O termo
'livre de contexto' vem da idéia de que um não-terminal V sempre pode ser trocado por w, sem precisar entender seu
contexto.
Um exemplo que facilita a compreensão do que são linguagens livres de contexto é a frase:
•• 1. "Fui à biblioteca hoje."
a palavra "biblioteca" independente da frase indica todo espaço (concreto, virtual ou híbrido) destinado a uma
coleção de informações de quaisquer tipos, sejamescritas em folhas de papel (monografias, enciclopédias,
dicionários, manuais, etc) ou ainda digitalizadas e armazenadas em outros tipos de materiais, tais como CD, fitas,
VHS, DVD e bancos de dados.
Por sua vez, na frase:
•• 2. "Fui à sede beber água porque estava com sede."
a palavra "sede" em sua segunda ocorrência indica uma sensação de caráter geral, iniciada por estímulos originados
dentro do próprio organismo e não do meio ambiente. Na primeira aparição, a palavra "sede" é um centro
administrativo.
Dessa forma, na frase 1 a palavra biblioteca é livre de contexto, e a palavra sede da frase 2 é dependente de
contexto.
Define-se uma linguagem formal como livre-de-contexto se existe uma gramática livre-de-contexto que a produz.
As gramáticas livres-de-contexto são bastante potentes para descrever a sintaxe da maioria das linguagens de
programação, necessitando as vezes algumas extensões ; a sintaxe da maioria das linguagens de programação são na
verdade especificadas usando gramáticas livres-de-contexto. Essas gramáticas são no entanto bastante simples para
permitir a criação de analisadores eficientes, os quais, por uma cadeia definida, determinam como elas podem ser
geradas a partir da gramática.
Linguagens livres de contexto 5
La BNF (Backus Naur form) é a notação usada com mais frenqüência para descrever uma gramática
livre-de-contexto.
Aplicações
Tais linguagens são importantes para definir linguagens de programação. Por exemplo, as linguagens que requerem o
balanceamento de parênteses são geradas pela gramática . Da mesma forma, a maioria das
expressões aritméticas é gerada por gramáticas livres de contexto.
Exemplos
Uma linguagem livre de contexto é , a linguagem de todas as cadeias de caracteres não
vazias de tamanho par, as primeiras metades sempre preenchidas com " s" e as segundas metades sempre
preenchidas com " "s. é gerada pela gramática , e é aceita pelo autômato de pilha
, em que é definido da seguinte forma:
Em que z é a pilha de símbolos inicial e x significa desempilhar.
Propriedades de fechamento
Linguagens livres de contexto são fechadas nas seguintes operações. Se L e P são linguagens livres de contexto e D é
uma linguagem regular, as seguintes linguagens também são livres de contexto:
• o fecho de Kleene de L
• a imagem φ(L) de L sob um homorfismo φ
• a concatenação de L e P
• a união de L e P
• a interseção (com uma linguagem regular) de L e D
Linguagens livres de contexto não são fechadas em complemento, interseção ou diferença.
Propriedades de decisão
Os seguintes problemas são indecidíveis para quaisquer gramáticas livres de contexto A e B:
• ?
• ?
• ?
• ?
Os seguintes problemas são decidíveis para quaisquer linguagens livres de contexto:
• ?
• é finito?
• Dada a palavra w, ?
Análise léxica 6
Análise léxica
Análise léxica é o processo de analisar a entrada de linhas de caracteres (tal como o código-fonte de um programa de
computador) e produzir uma seqüência de símbolos chamado "símbolos léxicos" (lexical tokens), ou somente
"símbolos" (tokens), que podem ser manipulados mais facilmente por um parser (leitor de saída). O componente do
compilador responsável pela execução desse processo é conhecido como Analisador léxico.
O analisador léxico, ou scanner como também é chamado, faz a varredura do programa fonte caractere por caractere
e, traduz em uma seqüência de símbolos léxicos ou tokens. É nessa fase que são reconhecidas as palavras reservadas,
constantes, identificadores e outras palavras que pertencem a linguagem de programação. O analisador léxico
executas outras tarefas como por exemplo o tratamento de espaços, eliminação de comentários, contagem do número
de linhas que o programa possui e etc.
A análise léxica é a forma de verificar determinado alfabeto. Quando analisamos uma palavra, podemos definir
através da análise léxica se existe ou não algum caractere que não faz parte do nosso alfabeto, ou um alfabeto
inventado por nós.
É a primeira etapa do processo de compilação e seu objetivo é dividir o código fonte em símbolos, preparado-o para
a Análise Sintática. Neste processo pode-se destacar três atividades como fundamentais:
•• Extração e classificação dos tokens;
•• Eliminação de delimitadores e comentários;
•• Recuperação de Erros.
O analisador léxico funciona de duas maneiras:
• Primeiro estado da análise: A primeira etapa lê a entrada de caracteres, um de cada vez, mudando o estado em
que os caracteres se encontram. Quando o analisador encontra um caracter que ele não identifica como correto,
ele o chama de "estado morto" então, ele volta à última análise que foi aceita e assim tem o tipo e comprimento do
léxico válido.
Um léxico, entretanto, é uma única lista de caracteres conhecidas de ser um tipo correto. Para construir um símbolo,
o analisador léxico necessita de um segundo estado.
• Segundo estado da análise: Nesta etapa são repassados os caracteres do léxico para produzir um valor. O tipo do
léxico combinado com seu valor é o que adequadamente constitui um símbolo, que pode ser dado a um parser.
(Alguns símbolos tais como parênteses não têm valores, e então a função da análise pode retornar nada).
A análise léxica escreve um parser muito mais fácil. Em vez de ter que acumular, renomeia seus caracteres
individualmente. O parser não mais se preocupa com símbolos e passa a preocupar-se só com questões de sintática.
Isto leva a eficiência de programação, e não eficiência de execução. Entretanto, desde que o analisador léxico é o
subsistema que deve examinar cada caracter único de entrada, podem ser passos intensivos e o desempenhos se torna
crítico, pode estar usando um compilador.
As desvantagens da Análise Léxica são o tratamento de dados em branco, formato fixo de entrada e a inexistência de
palavras reservadas, em determinadas linguagens.
Análise léxica 7
Analisador Léxico
A implementação de um analisador léxico requer uma descrição do autômato que reconhece as sentenças da
gramática ou expressão regular de interesse com os seguintes procedimentos auxiliares:
• Estado-Inicial: Recebe como argumento a referência para o autômato e retorna o seu estado inicial;
• Estado-final: Recebe como argumentos a referência para o autômato e a referência para o estado corrente. O
procedimento retorna true (verdadeiro) se o estado especificado é elemento do conjunto de estados finais do
autômato, ou false (falso) caso contrário; e
• Próximo-Estado: Recebe como argumento a referência para o autômato, para o estado corrente e para o símbolo
sendo analisado. O procedimento consulta a tabela de transições e retorna o próximo estado do autômato, ou o
valor nulo se não houver transição possível.
Nesta fase do processo de compilação, uma sequência de caracteres do código do programa é lida e agrupada em
uma sequência de tokens.
Erros léxicos
•• Poucos erros podem ser detectados durante a análise léxica dada a visão restrita desta fase,
como mostra o exemplo: fi(a== f(x)) outro_and;
•• fi é a palavra chave if grafada incorretamente?
• fi é um identificador de função que não foi declarada faltando assim um separador (‘;’)
entre a chamada da função fi e o comando seguinte (outro_and)?
•• Ainda assim o analisador léxico pode não conseguir prosseguir dado que a cadeia encontrada não se enquadra em
nenhum dos padrões conhecidos.
•• Para permitir que o trabalho desta fase prossiga mesmo com a ocorrência de erros deve ser implementada uma
estratégia de recuperação de erros
Recuperação de erros
Ações possíveis:
•• remoção de sucessivos caracteres até o reconhecimento de um token válido (modalidade Pânico).
•• inserção de um caractere ausente.
•• substituição de um caractere incorreto por outro correto.
•• transposição de caracteres adjacentes.
Tais estratégias poderiam ser aplicadas dentro de um escopo limitado(denominado erros de distância mínima).
While(a<100) {fi(a==b) break else a++;} ...
Estas transformações seriam computadas na tentativa de obtenção de um programa sintaticamente correto (o que não
significa um programa correto, daí o limitado número de compiladores experimentais que usam tais técnicas).
Análise léxica 8
Tokens
O objetivo principal da análise léxica é identificar sequências de caracteres que constituem unidades léxicas
("tokens"). O analisador léxico lê, caractere a caractere, o texto fonte, verificando se os caracteres lidos pertencem
ao alfabeto da linguagem, identificando tokens, e desprezando comentários e brancos desnecessários.
Os tokens constituem classes de símbolos tais como palavras reservadas, delimitadores, identificadores, etc., e
podem ser representados, internamente, através do próprio símbolo (como no caso dos delimitadores e das palavras
reservadas) ou por um par ordenado, no qual o primeiro elemento indica a classe do símbolo, e o segundo, um índice
para uma área onde o próprio símbolo foi armazenado (por exemplo, um identificador e sua entrada numa tabela de
identifidadores).
Além da identificação de tokens, o Analisador Léxico, em geral, inicia a construção da Tabela de Símbolos e envia
mensagens de erro caso identifique unidades léxicas não aceitas pela linguagem em questão. A saída do Analisador
Léxico é uma cadeia de tokens que é passada para a próxima fase, a Análise Sintática. Em geral, o Analisador
Léxico é implementado como uma subrotina que funciona sob o comando do Analisador Sintático.
O analisador léxico tem a função de ler uma sequência de caracteres que constitui um programa fonte e coleta, dessa
sequência, os tokens que constituem esse programa.
Os tokens (símbolos léxicos) são unidades básicas de texto do programa. Eles são representados internamente por
três informações: classe do token, valor do token e posição do token.
Classe do token – representa o tipo do token conhecido.
Valor do token – dependente da classe; podem ser divididos em dois grupos: tokens simples e tokens com
argumento.
Posição do token – indica o local do texto fonte (linha e coluna) onde ocorreu o token.
Tokens simples – não tem um valor associado, por exemplo como as palavras reservadas.
Tokens com argumento – possuem um valor associado, por exemplo como os identificadores e as constantes.
As classes para palavras reservadas constituem-se em abreviações dessas, não sendo necessário passar seus valores
para o analisador sintático.
É usual que os compiladores representem a classe de um token por um número inteiro para tornar a representação
mais compacta.
Especificação
Os analisadores léxicos são, usualmente, especificados através de notações para a descrição de linguagens regulares
tais como autômatos finitos, expressões regulares ou gramáticas regulares.
A especificação descreve o conjunto de tokens que formam a linguagem. Fazem parte dessa especificação sequências
de caracteres que podem aparecer entre tokens e devem ser ignoradas tais como espaços em branco e comentários.
Na grande maioria das linguagens de programação, as palavras reservadas da linguagem são casos particulares dos
identificadores e seguem a mesma notação desses.
As especificações de analisadores léxicos, usualmente, não expressam, explicitamente, o reconhecimento de palavras
reservadas. Todas essas palavras então são armazenadas em uma tabela interna, que é examinada cada vez que um
identificador é reconhecido. Se esse identificador ocorre na tabela, então trata-se de uma palavra reservada, caso
contrário, trata-se de um identificador.
Em geral, o analisador léxico e o analisador sintático formam um único passo, porém, o analisador léxico pode
constituir um passo individual do compilador. Quando atuam em um único passo, o analisador léxico atua como uma
subrotina que é chamada pelo analisador sintático sempre que este necessita de mais um token.
Existem motivos que levam a dividir (conceitualmente) a análise do programa em análise léxica e sintática:
Análise léxica 9
1) Simplificação e modularização do projeto do compilador;
2) Os tokens podem ser descritos utilizando-se notações simples (como expressões regulares), enquanto a estrutura
sintática de comandos e expressões das linguagens de programação requer uma notação mais expressiva, como as
gramáticas livres do contexto;
3) Os reconhecedores construídos a partir da descrição dos tokens (através de gramáticas regulares, expressões
regulares ou autômatos finitos) são mais eficientes e compactos do que os reconhecedores construídos a partir das
gramáticas livres de contexto.
Implementação
A implementação de analisadores léxicos é feita, em geral, através de uma tabela de transição, a qual indica a
passagem de um estado a outro pela leitura de um determinado caractere. Através do uso de um gerador de
analisadores léxicos, podem ser gerados a tabela e o correspondente programa de controle. No caso do gerador, o
próprio projetista especifica os tokens a serem reconhecidos através de expressões regulares e a partir dessas
expressões, o gerador gera um programa que implementa o analisador léxico correspondente.
Existe outra forma (alternativa) para se implementar um analisador léxico que é através da construção de um
programa que simule o funcionmento do autômato correspondente.
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, de procedimentos, etc) definidos no programa fonte. Essa
tabela associa atributos (tipo, escopo, limites no caso de vetores e número de parâmetros no caso de funções) aos
nomes que foram definidos pelo programador. A construção dessa tabela, em geral, se dá durante a análise léxica,
quando os identificadores são reconhecidos.
Quando um identificador é encontrado pela primeira vez, o analisador léxico armazena-o na tabela sem condições
ainda de associar atributos a esse identificador. Geralmente essa tarefa é executada durante as fases de análise
sintática e semântica.
Existem vários modos de se organizar e acessar as tabelas de símbolos sendo, os mais comuns:
1) Listas lineares - é o mecanismo mais simples, mas seu desempenho é pobre quando o número de consultas é
elevado.
2) Árvores binárias
3) Tabelas hash - têm melhor desempenho, mas exigem mais memória e esforço de programação.
Análise sintática 10
Análise sintática
Exemplo da análise sintática de uma expressão matemática. O
resultado é uma árvore da expressão
Análise sintática (também conhecido pelo termo em
inglês parsing) é o processo de analisar uma sequência
de entrada (lida de um arquivo de computador ou do
teclado, por exemplo) para determinar sua estrutura
gramatical segundo uma determinada gramática formal.
Essa análise faz parte de um compilador, junto com a
análise léxica e análise semântica. A análise sintática,
ou análise gramatical é o processo de se determinar se
uma cadeia de símbolos léxicos pode ser gerada por
uma gramática.
A análise sintática transforma um texto na entrada em uma estrutura de dados, em geral uma árvore, o que é
conveniente para processamento posterior e captura a hierarquia implícita desta entrada. Através da análise léxica é
obtido um grupo de tokens, para que o analisador sintático use um conjunto de regras para construir uma árvore
sintática da estrutura.
Em termos práticos, pode também ser usada para decompor um texto em unidades estruturais para serem organizadas
dentro de um bloco, por exemplo.
A vasta maioria dos analisadores sintáticos implementados em compiladores aceitam alguma linguagem livres de
contexto para fazer a análise. Estes analisadores podem ser de vários tipos, como o LL, LR e SLR.
Tipos de analisadores sintáticos
A tarefa do analisador sintático (um algoritmo, programa de computador ou componente de software que se preze a
realizar uma análise sintática) é essencialmente a de determinar se uma entradade dados pode ser derivada de um
símbolo inicial com as regras de uma gramática formal. Isso pode ser feito de duas maneiras:
• Descendente (top-down) - um analisador pode iniciar com o símbolo inicial e tentar transformá-lo na entrada de
dados. Intuitivamente, o analisador inicia dos maiores elementos e os quebra em elementos menores. Exemplo:
analisador sintático LL.
• Ascendente (bottom-up) - um analisador pode iniciar com um entrada de dados e tentar reescrevê-la até o símbolo
inicial. Intuitivamente, o analisador tentar localizar os elementos mais básicos, e então elementos maiores que
contêm os elementos mais básicos, e assim por diante. Exemplo: analisador sintático LR.
Determinismo
Analisadores sintáticos são determinísticos se sempre souberem que ação tomar independentemente da entrada de
texto. Este comportamento é desejado e esperado na compilação de uma linguagem de programação. No campo de
processamento de linguagem natural, pensava-se que essa a análise sintática determinística era impossível devido à
ambiguidade inerente das linguagens naturais, em que diversas sentenças possuem mais de uma possível
interpretação. Entretanto, Mitch Marcus propôs em 1978 o analisador sintático Parsifal, que consegue lidar com
ambiguidades ainda que mantendo o comportamento determinístico.
Análise sintática 11
Funcionamento
Um analisador sintático para uma gramática G é um programa que aceita como entrada uma sentença (uma lista de
símbolos S) e constrói para a sentença sua árvore gramatical (ou equivalentemente uma seqüência de derivação) ou,
caso a sentença não pertença à linguagem descrita por G, uma indicação de erro.
Duas técnicas básicas para a construção de analisadores sintáticos são a construção ascendente ou a construção
descendente. Na construção ascendente (bottom-up), o analisador sintático varre a sentença buscando aplicar
produções que permitam substituir seqüências de símbolos da sentença pelo lado esquerdo das produções, até
alcançar como único símbolo restante o símbolo inicial da gramática.
O Algoritmo a seguir ilustra a estratégia de reconhecimento de sentença baseado em construção ascendente da árvore
sintática. Esse algoritmo recebe como entrada uma representação da gramática G e a lista S de símbolos terminais
que compõem a sentença. A saída é uma indicação se a sentença S pertence (true) ou não (false) à gramática G. Para
a descrição desse algoritmo, as seguintes funções são definidas:
que recebe uma gramática G como argumento e retorna o seu símbolo sentencial; e
MATCH(G, ) retorna uma nova lista de símbolos gerada a partir da aplicação de alguma regra de G à sentença S.
Para tanto, esse procedimento analisa se para a sentença S, composta pela seqüência de símbolos x1x2...xn 
(onde eventualmente e podem ser a cadeia vazia), há na gramática alguma produção aplicável 
x1x2...xn. Se houver, o valor de retorno do procedimento é a lista ; caso contrário, o procedimento retorna
uma lista vazia.
Na construção descendente (top-down), o objetivo é iniciar a análise com uma lista que contém inicialmente apenas
o símbolo sentencial; a partir da análise dos símbolos presentes na sentença, busca-se aplicar regras que permitam
expandir os símbolos na lista até alcançar a sentença desejada.
Na construção descendente, o objetivo é obter uma derivação mais à esquerda para uma sentença. Em termos de
árvores gramaticais, a construção descendente busca a construção de uma árvore a partir da raiz usando pré-ordem
para definir o próximo símbolo não-terminal que deve ser considerado para análise e expansão.
Pela forma como a técnica de construção descendente opera, ela não pode ser aplicada a gramáticas com produções
recursivas à esquerda, ou seja, que contenham regras da forma:
A A 
A limitação é que a análise descendente de tal tipo de produção poderia levar a uma recursão infinita na análise pela
tentativa de expandir sempre a mesma regra sem consumir símbolo algum da entrada.
É possível transformar uma produção recursiva à esquerda em uma recursiva à direita que descreve as mesmas
sentenças através da seguinte técnica. Sejam e duas seqüências de símbolos que não sejam iniciadas pelo
símbolo não-terminal A e sejam as produções para A:
A A A 
Através da introdução de um novo símbolo não-terminal A’, as mesmas sentenças descritas pelas produções acima
podem ser descritas pelas produções recursivas à direita:
A A’
A’ A’
A’ 
Nos dois casos, as sentenças são formadas por uma ocorrência de no início seguida por zero ou mais ocorrências
de .
Os primeiros compiladores usavam essencialmente dois tipos de analisadores sintáticos. Analisadores baseados em
precedência de operadores utilizam a técnica de construção ascendente combinada com informação sobre a
precedência e associatividade de operadores da linguagem para guiar suas ações, sendo adequados à análise de
Análise sintática 12
expressões aritméticas. Analisadores do tipo descendentes recursivos implementam a técnica de construção
descendente através de um conjunto de rotinas mutuamente recursivas para realizar a análise, sendo normalmente
utilizados para outros comandos que não expressões aritméticas.
Analisador sintático preditivo
Esta seção descreve a construção de um analisador sintático baseado na técnica de construção descendente. Este
programa, que realiza a análise sintática preditiva não recursiva, recebe como argumentos uma descrição da
gramática G e a sentença S, expressa na forma de uma lista de símbolos terminada com um símbolo delimitador $,
não-pertencente aos símbolos da gramática.
O ponto crítico nesse procedimento é saber escolher, dado um símbolo não-terminal que pode ser expandido e os
próximos símbolos da sentença, qual deve ser a produção da gramática que deve ser aplicada na expansão. A tabela
sintática para a gramática, cuja construção é descrita na seqüência, contém essa informação essencial à execução do
algoritmo.
Construção da tabela sintática
A tabela sintática é a estrutura de apoio ao reconhecimento de sentenças pela técnica de construção descendente que
tem como chave um par de símbolos. O primeiro componente da chave é um símbolo não-terminal, que corresponde
ao símbolo que estará sendo analisado pelo algoritmo de reconhecimento da sentença. O segundo componente da
chave é um símbolo da sentença, ou seja, um símbolo terminal ou o delimitador de fim de seqüência $. O valor
associado a esse par de símbolos é a produção da gramática a ser aplicada para prosseguir com o reconhecimento da
sentença.
Para construir a tabela sintática para uma gramática qualquer G, deve-se analisar cada uma das produções A 
de G. Inicialmente, deve-se obter o conjunto de símbolos terminais que podem iniciar uma cadeia a partir de S. Se S
for um símbolo terminal, então esse conjunto é composto apenas por esse próprio símbolo. Caso contrário, as
possíveis expansões de S devem ser analisadas até que os símbolos terminais ou a string vazia sejam alcançados.
Caso símbolos terminais sejam alcançados, a tabela sintática recebe a entrada com o valor A para cada chave
A,t onde t é cada um dos símbolos terminais que podem ser alcançados desde S. Caso a string vazia seja um dos
resultados possíveis para a expansão de S, é preciso analisar também as possíveis expansões dos símbolos à direita
do símbolo corrente na produção.
O cômputo de STF pode também ser aplicado a uma cadeia de símbolos, sendo que neste caso o valor resultante é o
primeiro cômputo de STF aplicado a cada símbolo da seqüência tal que o resultado não tenha sido . Caso o
cômputo de STF para todos os símbolos da cadeia resulte em , este também será o resultado final.
O outro procedimento auxiliar deve computar, para cada símbolo não-terminal da gramática G, o conjunto de
símbolos terminais que podem estar imediatamente à direita do símbolo especificado em alguma forma sentencial.
Essa informação é mantida em uma lista seguinte( ), onde S é osímbolo de interesse. Para construir essas listas,
as seguintes regras devem ser aplicadas até que se esgotem as possibilidades de acrescentar algo às listas:
Regra 1: O símbolo sentencial da gramática pode ter como próximo símbolo o delimitador de fim de sentença; insira
o símbolo $ na lista seguinte( (G)).
Regra 2: Se existir uma produção em G da forma A B , então todos os símbolos terminais que podem
iniciar a expansão de podem aparecer após B; insira em seguinte(B) o conteúdo de STF( ) sem incluir , se
estiver presente.
Regra 3: Se existir uma produção em G da forma A B, então B termina a expansão de A. O mesmo pode
ocorrer para uma produção da forma A B onde a expansão de pode levar à string vazia . Em
qualquer um desses casos, tudo que está em seguinte(A) deve ser incluído em seguinte(B).
Análise sintática 13
Com esses procedimentos, a construção da tabela sintática para uma gramática G procede como se segue. Para cada
produção A em G:
1. Compute STF(G, ). Para cada símbolo x dessa lista, acrescente a produção A como o valor da tabela para
o par de chaves [A,x].
2. Caso STF(G, ) contenha , acrescente a produção A à tabela para o par de chaves [A,y] para cada y em
seguinte(A).
Como exemplo, considere a construção do analisador sintático preditivo para a gramática vista anteriormente. Como
essa gramática é recursiva à esquerda, o primeiro passo nessa construção é construir a gramática equivalente sem
esse tipo de recursão. O resultado da aplicação da técnica para eliminar a recursão à esquerda resulta na seguinte
gramática:
E TE'
E' +TE'
E' 
T FT'
T' xFT'
T' 
F (E)
F id
Para esta gramática, o cômputo de STF() para cada um dos símbolos não-terminais resulta em:
STF(E) = STF (T) = STF (F) = (,ID
STF (E') = +, 
STF (T') = $, 
Como STF() para um símbolo terminal é o próprio símbolo, esses valores não são aqui apresentados.
A aplicação das regras para a construção das listas seguinte() resulta, para cada símbolo não-terminal:
seguinte(E) = seguinte(E') =$,)
seguinte(T) = seguinte(T') = +,$,)
seguinte(F) = x,+,$,)
A construção da tabela sintática para essa gramática analisa cada uma das suas produções:
P1. E TE'
Para essa produção, STF(TE')= STF(T), que resulta na lista com os símbolos ( e id. Portanto, na tabela sintática as
entradas [E,(] e [E,id] farão referência à produção P1.
P2. E' +TE'
O cômputo de STF(+TE') resulta em +, ou seja, haverá uma referência para P2 na entrada [E',+] da tabela sintática.
P3. E' 
STF( )= ; portanto, a segunda regra para a construção da tabela deve ser aplicada. Como seguinte(E')=$,), as
entradas correspondentes à [E',$] e [E',)] farão referência à produção P3.
P4. T FT'
Como STF(FT')= STF(F)=(,id, as entradas para [T,(] e [T,id] farão referência à P4.
P5. T' xFT'
Neste caso, STF(xFT')=x. Portanto, a entrada [T',x] terá a referência para P5.
P6. T' 
Análise sintática 14
Novamente a segunda regra deve ser aplicada. Como seguinte(T')=+,$,), as entradas com referência à P6 na tabela
serão [T',+], [T',$] e [T',)].
P7. F (E)
Apenas a entrada [F,(] fará referência a esta produção, pois STF((E))= (.
P8. F id
Apenas a entrada [F,id] fará referência a esta produção, pois STF(id)= id.
Para algumas gramáticas, pode ser que a tabela sintática apresente mais que uma produção por chave, o que reduz a
aplicabilidade desse tipo de analisador –– seria necessário manter um registro do estado do analisador em pontos de
múltipla escolha para eventualmente retornar a esse estado e tentar a outra alternativa, em caso de insucesso na
escolha anterior. Gramáticas ambíguas e gramáticas recursivas à esquerda são exemplos de gramáticas que
produziriam tabelas sintáticas com múltiplas produções para uma chave de par de símbolos.
Gramáticas com tabelas sintáticas sem múltiplas definições são denominadas gramáticas LL(1), indicando que a
varredura da sentença ocorre da esquerda para a direita (Left-to-right) e que é utilizada a derivação canônica mais à
esquerda (Leftmost derivation). O número entre parênteses indica quantos símbolos da sentença precisam ser
analisados (lookahead) para a tomada de decisão no processo de reconhecimento.
Uma gramática com duas produções A e A é LL(1) se apresentar as seguintes propriedades:
1. S e não podem derivar ao mesmo tempo seqüências que tenham início pelo mesmo símbolo terminal; 2.
Apenas um dos dois, S ou , podem derivar ; e 3. Se uma das produções deriva , a outra não pode derivar
qualquer seqüência de símbolos que tenha início com um símbolo presente em seguinte(A).
Geradores de analisadores sintáticos
Como ocorre na construção de analisadores léxicos, a construção de programas analisadores sintáticos é usualmente
suportada por ferramentas para a geração automática de programas a partir de uma especificação.
Uma tradicional ferramenta de criação de analisadores sintáticos é o yacc (Yet Another Compiler-Compiler), oriunda
do ambiente de desenvolvimento de software do sistema operacional Unix. Assim como a ferramenta lex, yacc
recebe como entrada um arquivo de especificação de uma gramática e gera como saída um módulo com código-fonte
em C contendo uma rotina que realiza o reconhecimento de sentenças segundo essa gramática.
Especificação da gramática
O arquivo de entrada para o yacc, que por convenção recebe a extensão .y, é estruturado em três seções. Como na
definição de arquivos lex, essas três seções –– definições, regras da gramática e código do usuário –– são separadas
pelos símbolos %%.
A especificação das regras da gramática utiliza uma notação próxima de BNF. A notação BNF (Backus-Naur Form)
introduz uma forma de representação textual para descrever gramáticas livres de contexto. BNF foi inicialmente
desenvolvida para especificar a linguagem Algol 60.
O principal operador dessa linguagem é o operador binário ::=, que permite descrever as produções da gramática. O
operando do lado esquerdo do operador é o símbolo não-terminal; do lado direito, a sua expansão, que pode conter
símbolos terminais e não-terminais.
Na notação BNF, os símbolos não-terminais são delimitados por colchetes angulares, < e >. Por exemplo, a regra
<expr> ::= (<expr>)
define que uma expressão entre parênteses (o lado direito do operador) pode ser reduzido simplesmente a uma
expressão (o símbolo não-terminal do lado esquerdo).
Análise sintática 15
A notação BNF também introduz um conjunto de operadores destinados a simplificar a representação das regras da
gramática, assim como lex introduz operadores para facilitar a especificação de expressões regulares.
O operador | (ou) permite expressar em uma mesma regra produções alternativas. Por exemplo, a regra
(S) ::= A|B
equivale às duas regras
(S) ::= A (S) ::= B
O operador [ ] (opcional) permite expressar zero ou uma ocorrência do símbolo especificado. Por exemplo, a regra
(S) ::= [a]
equivale a
(S) ::= (S) ::= a
O operador ( | ) (fatoração) permite expressar a combinação de símbolos alternativos, como em
(S) ::= a(b|c)d
que equivale a
(S) ::= abd (S) ::= acd
O operador * (repetição), assim como para expressões regulares, expressa 0 ou mais ocorrências do símbolo. Por
exemplo, a regra
(S) ::= a*
equivale a
(S) ::= (S) ::= a(S)
Assim, a ocorrência do padrão no lado direito de uma produção equivale a
|a|aa|aaa|aaaa|..... O operador {||}* (concatenação) permite expressar a repetição múltipla de símbolos
concatenados. Por exemplo, a regra
(S) ::= {A||B}*
equivale a
(S) ::= A(t) (t) ::= (t) ::= BA(t)
Cada produção no yacc é expressa na forma:
simb : exp ;
onde simb é um símbolo não terminal e exp é a sua expansão em termos de outros símbolos da gramática. A
expansão pode conter símbolos terminais e não-terminais, que por convenção são representados usando letras
maiúsculas e minúsculas, respectivamente.
Pelas características de gramáticas livres de contexto, a expansão pode ser recursiva, isto é, conter o próprio símbolo
que está sendo definido, como em
expr : expr '+'expr ;
Porém, pelo menos uma expansão para esse símbolo deve ser não-recursiva:
expr : IDENT ;
Em caso de definição recursiva, pelas características do analisador gerado (LR(1)) recomenda-se optar quando
possível pela recursão à esquerda.
Produções para um mesmo símbolo podem ser agrupadas usando o símbolo '|',
 expr : expr + expr
 | IDENT
 ;
Análise sintática 16
Expansões para a cadeia vazia podem ser definidas; por convenção e para tornar mais clara a definição, essa
expansão é destacada na forma de um comentário C:
 retv : /* empty */
 | expr
 ;
O símbolo sentencial da gramática pode ser estabelecido na seção de definições através da declaração start, como em
 %start expr
Na ausência dessa declaração, o símbolo não-terminal cuja expansão é definida na primeira produção da seção de
regras da gramática é assumido ser o símbolo sentencial.
Outros tipos de declaração que podem estar presentes na primeira seção são declarações em C, colocadas entre os
símbolos %{ e %}, e a definição dos nomes de tipos de tokens, os quais serão usados posteriormente nas expansões
das produções.
Tokens que são representados por um único caractere, como '+' ou ';', não precisam ser declarados e podem ser
usados dessa forma (como constantes do tipo caractere em C) nas expansões; os demais tokens precisam ser
explicitamente declarados. Para tanto, a declaração token pode ser utilizada, como em
 %token IDENT
Alternativamente, tokens para operadores podem ser definidos com uma especificação de associatividade usando, ao
invés de token, as declarações left, right ou nonassoc. Uma declaração
 %left OP
determina que uma expressão A OP B OP C será interpretada como (A OP B) OP C, enquanto que se a declaração
tivesse sido
 %right OP
a interpretação seria A OP (B OP C). A declaração
 %nonassoc OP
determinaria que a expressão A OP B OP C estaria incorreta, pois o operador não é associativo.
A precedência dos operadores também é definida através dessas declarações. Operadores definidos através da mesma
linha de declaração, como
 %left OP1 OP2 
têm a mesma precedência. Para aqueles definidos em linhas distintas, as últimas declarações têm maior precedência.
O símbolo terminal error é pré-definido, podendo ser utilizado como a última expansão de um símbolo caso a
aplicação deseje determinar um curso de ação específico em uma situação de não-reconhecimento de uma sentença a
partir das expansõbix previamente definidas para o símbolo.
Análise sintática 17
Manipulação das sentenças reconhecidas
Reconhecer que uma seqüência de símbolos é uma sentença válida em uma gramática, é parte essencial do processo
de compilação, porém pouco uso seria se simplesmente uma indicação de validade fosse retornada sem nenhuma
possibilidade de manipulação adicional das expressões. No caso do yacc, essa possibilidade está associada à
definição de ações semânticas.
Uma ação semântica em yacc é definida através de um bloco de expressões em C associado à definição de produções
para um símbolo não-terminal:
 symb : expansão { ação } ;
A definição do corpo da ação pode conter referências aos valores semânticos de cada um dos símbolos da produção.
O valor semântico de um token está associado a um valor associado ao símbolo, que pode ser por exemplo um valor
numérico de uma constante ou a string associada a um identificador.
O valor semântico do token pode ser referenciado na expressão C através de pseudo-variáveis com nome $i, onde i
determina a posição do token na expansão. A variável $$ referencia o valor semântico resultante para o símbolo
sendo definido. Por exemplo:
 expr : expr '+' expr
 { $$ = $1 + $3 }
 ;
atribui à expressão reduzida o valor semântico que é a soma dos valores semânticos do primeiro e do terceiro
componente da expansão, que estão separados pelo segundo componente, '+'.
Se nenhuma ação for definida para uma produção, a ação semântica padrão -- { $$ = $1; } é assumida.
O tipo associado a valores semânticos é definido pela macro YYSTYPE, que é inicialmente definida como int. Para
modificar esse padrão, pode-se modificar essa definição através de uma declaração C na primeira seção do arquivo
que define a gramática. Por exemplo:
 %{
 #define YYSTYPE double
 %}
Em aplicações que necessitem manipular tokens com diferentes tipos de valores semânticos, a declaração union deve
ser utilizada para definir quais são os tipos de valores possíveis. Por exemplo, em uma aplicação que manipula
valores inteiros e reais a seguinte declaração estaria presente:
 %union {
 int ival;
 double fval;
 }
Essa declaração determina que a coleção de tipos de valores permitidos é composta por valores com nome ival ou
fval, respectivamente para valores inteiros e reais –– no código C, uma união será criada. Esses mesmos nomes são
utilizados para qualificar a definição de tokens da gramática, como em
 %token <ival> INTEGER
 %token <fval> REAL
Quando uma coleção de tipos de valores é utilizada, é preciso determinar também qual o tipo para o símbolo
não-terminal para o qual a expressão está sendo reduzida. Para esse fim, yacc define a declaração type:
 %type <fval> expr
Tradução dirigida por sintaxe 18
Tradução dirigida por sintaxe
Tradução dirigida por sintaxe é uma técnica de especificação de compiladores tradutores que permite associar
ações semânticas às regras da gramática. Essa técnica é utilizada em quase todos os compiladores modernos.
Nesta tradução do código de alto nível para o do processador está a associação para traduzir para a linguagem-alvo,
obtida para as diversas expressões do program, a representação da árvore Gramatical.
Embora tal atividade possa ser realizada ao fim da análise sintática para toda árvore, ela é realmente concluída com
as ações semânticas, numa associação à aplicação das regras de reconhecimento do analisador sintático.
Para se gerar o código, não é necessário ir diretamente para a linguagem assembly do processador-alvo, pois o
analisador sintático gera o mesmo com uma linguagem próxima para uma máquina abstrata, isso independente de
processadores específicos. Depois esse código produzido traduz para a linguagem assembly desejada que se deseja.
Ganha-se que grande parte do compilador é liberada para ser reaproveitada em trabalhos de tipos de processadores
díspares.
Na tradução dirigida pela sintaxe assume-se que os terminais tenham somente atributos sintetizados quando as
definições não apresentam nenhuma regra semântica . Esses atributos são bastante usados na prática, onde uma
sintaxe com os mesmos é chamada de definição S-atribuída.
Os valores para os atributos dos terminais geralmente são fornecidos por este léxico:
F ->dígito F.val= dígito.lexval
Eles costumam ser avaliados de baixo para cima, das folhas para a raiz.
Em resumo:
> É uma tradução de linguagens guiada por gramáticas livres de contexto;
> faz uma amarração de atributos, que representam valores (tipo, endereço, etc.), aos símbolos gramaticais de regras
de produção, onde são representada as construções de linguagens;
> Faz associação de regras semânticas às produções, a fim de realizar cálculos dos valores de atributos amarrados.
Definições Dirigidas pela Sintaxe
São definições que usam uma gramática livre, onde cada símbolo gramatical possui um conjunto associado de
atributos.
Atributos do Símbolo
•• Sintetizados: O valor é computado a partir dos valores dos atributos dos filhos do nó.
•• Herdados: O valor é computado a partir dos valores dos atributos do pai e dos irmãos do nó.
Formas de Representação Intermediária
Normalmente a tradução do programa fonte no programa objeto é feita em dois passos, e uma representação
intermediária do programa fonte deve ser construída. Tipicamente, essa representação intermediária assume uma de
duas formas: uma árvore (árvore sintática) ou umaseqüência de comandos em uma linguagem intermediária, cuja
forma mais comum é a de quádruplas, ou seja, de comandos compostos de um operador e de três operandos, que
quase sempre são endereços. Por exemplo, o comando de atribuição:
x: = (a+b) * c
Tradução dirigida por sintaxe 19
Árvores Sintáticas
A primeira árvore que se poderia usar como base para uma representação intermediária é a árvore de derivação
correspondente ao programa fonte, de acordo com a gramática da linguagem fonte usada na construção do analisador
sintático. Entretanto, essa árvore de derivação em geral não é conveniente, uma vez que inclui muitos detalhes
desnecessários, que foram incluídos na sintaxe da linguagem fonte para resolver problemas de ambigüidade,
precedência, pontuação, clareza, legibilidade, etc. Por essa razão, em alguns casos é conveniente a definição de uma
segunda sintaxe, chamada sintaxe abstrata, que ignora todos os aspectos não essenciais da sintaxe original, conhecida
como sintaxe concreta.
Análise semântica
Chama-se análise semântica a terceira fase da compilação, onde se verificam os erros de semântica no código fonte
e se faz a coleta das informações necessárias para a próxima fase da compilação, a geração de código objeto. O
objetivo da análise semântica é trabalhar no nível de inter-relacionamento entre partes distintas do programa[1].
Objetivo da Análise Semântica
A análise sintática verifica e aponta as expressões que quebram qualquer regra determinada pela gramática. Mas o
nível de complexidade aumenta quando tratamos de liguagens dependentes de contexto. A análise semântica busca
apontar (não resolver) este tipo de erros (dependentes de contexto).
O objetivo da análise semântica é, assim, criar, a partir de um texto-fonte, uma interpretação expressa em alguma
notação adequada - geralmente uma linguagem intermediária do compilador. Isto é feito com base nas informações
das tabelas e nas saídas dos outros analisadores. Denomina-se semântica de uma sentença o significado por ela
assumido dentro do contexto em que se encontra. Semântica de uma linguagem é a interpretação que se pode atribuir
ao conjunto de todas as suas sentenças.
A análise semântica utiliza-se da verificação de tipos para verificar se um determinado operando recebe outro
operando de mesmo tipo. Um exemplo comum nas linguagens de programação é a análise semântica retornar um
erro quando uma variável do tipo numérica (real ou inteira) receber um valor tipo texto (string).
Um compilador usa uma tabela de símbolos (TS) para guardar informações sobre os nomes declarados em um
programa. A TS é pesquisada cada vez que um nome é encontrado no programa fonte. Alterações são feitas na TS
sempre que um novo nome ou nova informação sobre um nome já existente é obtida. A gerência da TS de um
compilador deve ser implementada de forma a permitir inserções e consultas da forma mais eficiente possível, além
de permitir o crescimento dinâmico da mesma.Cada entrada na ts é a declaração de um nome. Cada entrada na ts
pode ser implementada como um registro ( record ou struct) contendo campos (nome, tipo, classe, tamanho, escopo,
etc.) que a qualificam.
Exemplos típicos de erros semânticos são:
•• Uma variável não declarada
•• Uma multiplicação entre tipos de dados diferentes
•• Atribuição de um literal para outro tipo, como um inteiro em uma string ou vice-versa.
Análise semântica 20
Checagem de Tipo
O processo de verificação dos tipos ocorre em tempo de compilação (checagem estática) ou em tempo de execução
(checagem dinâmica). As linguagens de programação conforme a especificação da linguagem podem ser referidas
como de tipagem forte e tipagem fraca, influenciando o processo de checagem de tipo.
Tipagem estática
É quando acontece durante a copilação, pode ser por meio de alocação, desalocação de variaveis locais ou descoberta
de tipos em expressões numericas.
Tipagem dinâmica
É quando acontece durante a execução, pode ser por meio de alocação na heap com malloc (em C), verificação de
cast de classe (em java), ou verificação de limites de arrays (em java).
Bibliografia e referências
Referências
[1] http:/ / www. dca. fee. unicamp. br/ cursos/ EA876/ apostila/ HTML/ node71. html
Bibliografia
• Análise semântica- Programação de Sistemas- Ivan Luiz Marques Ricarte (http:/ / www. dca. fee. unicamp. br/
cursos/ EA876/ apostila/ HTML/ node71. html)
• Uso de Prolog no Desenvolvimento de Compiladores - Arquiteturas Especiais de Computadores - André Luis
Martinotto (http:/ / www. inf. ufrgs. br/ procpar/ disc/ cmp135/ trabs/ martinotto/ trabII/ semantica. htm)
Esta página é somente um esboço.
Ampliando-a você ajudará a melhorar o Wikilivros.
Geração de código intermediário 21
Geração de código intermediário
Tomando um compilador como um modelo de análise-e-síntese, podemos considerar que seus módulos iniciais
("front-end modules") traduzem um programa fonte em uma representação (linguagem) intermediária, a partir da
qual seus módulos finais ("back-end modules") geram o código objeto final.
A geração de um código intermediário requer um passo a mais para a tradução, tornando o processo um pouco mais
lento. Embora um programa fonte possa ser traduzido diretamente para a linguagem objeto, o uso de uma
representação intermediária, independente de máquina, tem as seguintes vantagens:
1.1. Reaproveitamento de código, facilitando o transporte de um compilador para diversas plataformas de hardware;
somente os módulos finais precisam ser refeitos a cada transporte.
2.2. Um otimizador de código independente de máquina pode ser usado no código intermediário.
Pode-se pensar nessa representação intermediária como um programa para uma máquina abstrata. A representação
intermediária deve possuir duas propriedades importantes: ser fácil de produzir e fácil de traduzir no programa alvo.
A diferença básica entre o código intermediário e o código objeto final é que não são especificados detalhes da
máquina alvo, tais como quais registradores serão usados, quais endereços de memória serão referenciados, etc.
Tipos de código intermediário
• HIR – High Intermediate Representation
•• Usada nos primeiros estágios do compilador
•• Simplificação de construções gramaticais para somente o essencial para otimização/geração de código
• MIR – Medium Intermediate Representation
•• Boa base para geração de código eficiente
•• Pode expressar todas características de linguagens de programação de forma independente da linguagem
•• Representação de variáveis, temporários, registradores
• LIR – Low Intermediate Representation
•• Quase 1-1 para linguagem de máquina
•• Dependente da arquitetura
Representação
As formas de representação mais comuns dos tipos de códigos intermediários são:
•• HIR
•• Árvore e grafo de sintaxe
•• Notações Pós-fixada e Pré-fixada
•• Representações linearizadas
•• MIR
•• Árvore e grafo de sintaxe
•• Notações Pós-fixada e Pré-fixada:
•• Representações linearizadas
•• Código de três endereços:
•• Quádruplas
•• Triplas
•• Grafos acíclicos dirigidos (DAG)
•• LIR
Geração de código intermediário 22
•• Instruções assembler
Geração de código
A tradução do código de alto nível para o código do processador está associada a traduzir para a linguagem-alvo a
representação da árvore gramatical obtida para as diversas expressões do programa. Embora tal atividade possa ser
realizada para a árvore completa após a conclusão da análise sintática, em geral ela é efetivada através das ações
semânticas associadas à aplicação das regras de reconhecimento do analisador sintático. Este procedimento é
denominado tradução dirigida pela sintaxe.
Em geral, a geração de código não se dá diretamente para a linguagem assembly do processador-alvo. Por
conveniência, o analisador sintático gera código para uma máquina abstrata, com uma linguagem próxima ao
assembly, porém independente de processadoresespecíficos. Em uma segunda etapa da geração de código, esse
código intermediário é traduzido para a linguagem assembly desejada. Dessa forma, grande parte do compilador é
reaproveitada para trabalhar com diferentes tipos de processadores.
Várias técnicas e várias tarefas se reúnem sob o nome de Otimização. Alguns autores da literatura consideram que,
para qualquer critério de qualidade razoável, é impossível construir um programa “otimizador”, isto é, um programa
que recebe como entrada um programa P e constrói um programa P’ equivalente que é o melhor possível, segundo o
critério considerado. O que se pode construir são programas que melhoram outros programas, de maneira que o
programa P’ é, na maioria das vezes, melhor, segundo o critério especificado do que o programa P original. A razão
para essa impossibilidade é a mesma que se encontra quando se estuda, por exemplo em LFA (Linguagens Formais e
Autômatos), o “problema da parada”: um programa (procedimento, máquina de Turing, etc.) não pode obter
informação suficiente sobre todas as possíveis formas de execução de outro programa (procedimento, máquina de
Turing, etc.).
Normalmente, é muito fácil fazer uma otimização em um programa, ou seja, uma transformação que o transforma
em outro melhor. O difícil é sempre obter a informação necessária que garante que a otimização pode realmente ser
aplicada, sem modificar em nenhum caso o funcionamento do programa.
Não é prático tentar otimizar um programa tentando sucessivamente eliminar todos os seus comandos, um de cada
vez. Note que as condições para cada eliminação dependem do comando, de sua posição, e, de certa maneira, de
todos os comandos restantes do programa. Para construir um otimizador de utilidade na prática, faz-se necessário
identificar oportunidades para otimização que sejam produtivas em situações correntes.
Outro exemplo de otimização que é citado freqüentemente é a retirada de comandos de um comando de repetição
(um loop). Por exemplo, um comando cujo efeito é independente do loop, pode valer a pena retirá-lo do loop, para
que ele seja executado apenas uma vez, em vez das muitas vezes que se presume que será executado o código de
dentro do loop.
Depende muito da finalidade de um compilador o conjunto de otimizações que ele deve oferecer. Um compilador
usado em um curso introdutório de programação não precisa de otimização, porque os programas são executados
quase sempre apenas uma vez. (Em vez de otimização, este compilador deveria dar boas mensagens de erro, que
pudessem auxiliar os principiantes na linguagem.) Por outro lado, um programa que vai ser compilado uma vez e
executado muitas vezes deve ser otimizado tanto quanto possível. Neste caso estão programas de simulação, de
previsão do tempo, e a maioria das aplicações numéricas.
Um outro problema na otimização de código para um compilador é a quantidade de informação que se deseja
manipular. Pode-se examinar otimizações locais (em trechos pequenos de programas, por exemplo trechos sem
desvios, ou seja, trechos em linha reta), otimizações em um nível intermediário (as otimizações são consideradas
apenas em funções, módulos, ou classes, dependendo da linguagem) e otimizações globais (que consideram as
inter-relações de todas as partes de um programa). A maioria dos compiladores oferece algumas otimizações do
primeiro tipo, possivelmente combinadas com a fase de geração de código, e quando muito algumas otimizações de
Geração de código intermediário 23
nível intermediário.
A maneira de tratar a otimização pode ser extremamente pragmática. Em um programa grande, a execução gasta
90% em 10% do código, e 10% do tempo nos 90% do código restantes (A literatura menciona valores de 90%-10%
até 70%-30%.). Isto acontece porque em geral um programa é constituído de inicializações e finalizações, entre as
quais se encontra um conjunto de vários loops aninhados. O tempo maior de execução (“os 90%”) é gasto no loop
mais interno (“os 10%”). Existem ferramentas que registram o perfil de execução do programa (profilers), permitindo
a identificação dos trechos mais executados, e a otimização pode se concentrar nestes trechos, ignorando para fins de
otimização o restante do programa. Por essa razão muitos compiladores oferecem opções de compilação que podem
ser ligadas e desligadas no código fonte, indicando para o compilador os trechos que o programador considera
importantes o suficiente para serem otimizados. Por estas razões, muito do trabalho no desenvolvimento de técnicas
de otimização é dedicado aos loops.
Código intermediário
A linguagem utilizada para a geração de um código em formato intermediário entre a linguagem de alto nível e a
linguagem assembly deve representar, de forma independente do processador para o qual o programa será gerado,
todas as expressões do programa original. Duas formas usuais para esse tipo de representação são a notação posfixa e
o código de três endereços.
Código de três endereços
O código de três endereços é composto por uma seqüência de instruções envolvendo operações binárias ou unárias e
uma atribuição. O nome "três endereços está associado à especificação, em uma instrução, de no máximo três
variáveis: duas para os operadores binários e uma para o resultado. Assim, expressões envolvendo diversas
operações são decompostas nesse código em uma série de instruções, eventualmente com a utilização de variáveis
temporárias introduzidas na tradução. Dessa forma, obtém-se um código mais próximo da estrutura da linguagem
assembly e, conseqüentemente, de mais fácil conversão para a linguagem-alvo.
Uma possível especificação de uma linguagem de três endereços envolve quatro tipos básicos de instruções:
expressões com atribuição, desvios, invocação de rotinas e acesso indexado e indireto.
Instruções de atribuição são aquelas nas quais o resultado de uma operação é armazenado na variável especificada à
esquerda do operador de atribuição, aqui denotado por :=. Há três formas para esse tipo de instrução. Na primeira, a
variável recebe o resultado de uma operação binária:
 x := y op z
O resultado pode ser também obtido a partir da aplicação de um operador unário:
 x := op y
Na terceira forma, pode ocorrer uma simples cópia de valores de uma variável para outra:
 x := y
Por exemplo, a expressão em C
a = b + c * d;
seria traduzida nesse formato para as instruções:
 _t1 := c * d
 a := b + _t1
As instruções de desvio podem assumir duas formas básicas. Uma instrução de desvio incondicional tem o formato
Geração de código intermediário 24
 goto L
onde L é um rótulo simbólico que identifica uma linha do código. A outra forma de desvio é o desvio condicional,
com o formato
 if x opr y goto L
onde opr é um operador relacional de comparação e L é o rótulo da linha que deve ser executada se o resultado da
aplicação do operador relacional for verdadeiro; caso contrário, a linha seguinte é executada.
Por exemplo, a seguinte iteração em C
while (i++ <= k) 
 x[i] = 0;
x[0] = 0;
poderia ser traduzida para
_L1: if i > k goto _L2
 i := i + 1
 x[i] := 0 
 goto _L1
_L2: x[0] := 0
A invocação de rotinas ocorre em duas etapas. Inicialmente, os argumentos do procedimento são "registrados com a
instrução param; após a definição dos argumentos, a instrução call completa a invocação da rotina. A instrução
return indica o fim de execução de uma rotina. Opcionalmente, esta instrução pode especificar um valor de retorno,
que pode ser atribuído na linguagem intermediária a uma variável como resultado de call.
Por exemplo, considere a chamada de uma função f que recebe três argumentos e retorna um valor:
f(a, b, c);
Neste exemplo em C, esse valor de retorno não é utilizado. De qualquer modo, a expressão acima seria traduzida
para
 param a
 param b
 param c
 _t1 := call f,3
onde o número após a vírgula indica o número deargumentos utilizados pelo procedimento f. Com o uso desse
argumento adicional é possível expressar sem dificuldades as chamadas aninhadas de procedimentos.
O último tipo de instrução para códigos de três endereços refere-se aos modos de endereçamento indexado e indireto.
Para atribuições indexadas, as duas formas básicas são
 x := y[i]
 x[i] := y
As atribuições associadas ao modo indireto permitem a manipulação de endereços e seus conteúdos. As instruções
em formato intermediário também utilizam um formato próximo àquele da linguagem C:
 x := &y
 w := *x
 *x := z
Geração de código intermediário 25
A representação interna das instruções em códigos de três endereços dá-se na forma de armazenamento em tabelas
com quatro ou três colunas. Na abordagem que utiliza quádruplas (as tabelas com quatro colunas), cada instrução é
representada por uma linha na tabela com a especificação do operador, do primeiro argumento, do segundo
argumento e do resultado
Para algumas instruções, como aquelas envolvendo operadores unários ou desvio incondicional, algumas das colunas
estariam vazias.
Na outra forma de representação, por triplas, evita a necessidade de manter nomes de variáveis temporárias ao fazer
referência às linhas da própria tabela no lugar dos argumentos. Nesse caso, apenas três colunas são necessárias, uma
vez que o resultado está sempre implicitamente associado à linha da tabela.
Notação posfixa
A notação tradicional para expressões aritméticas, que representa uma operação binária na forma x+y, ou seja, com o
operador entre seus dois operandos, é conhecida como notação infixa. Uma notação alternativa para esse tipo de
expressão é a notação posfixa, também conhecida como notação polonesa, na qual o operador é expresso após seus
operandos.
O atrativo da notação posfixa é que ela dispensa o uso de parênteses. Por exemplo, as expressões
 a*b+c;
 a*(b+c);
 (a+b)*c;
 (a+b)*(c+d);
seriam representadas nesse tipo de notação respectivamente como
 a b * c +
 a b c + *
 a b + c *
 a b + c d + *
Instruções de desvio em código intermediário usando a notação posfixa assumem a forma
 L jump
 x y L jcc
para desvios incondicionais e condicionais, respectivamente. No caso de um desvio condicional, a condição a ser
avaliada envolvendo x e y é expressa na parte cc da própria instrução. Assim, jcc pode ser uma instrução entre jeq
(desvio ocorre se x e y forem iguais), jne (se diferentes), jlt (se x menor que y), jle (se x menor ou igual a y), jgt (se x
maior que y) ou jge (se x maior ou igual a y).
Expressões em formato intermediário usando a notação posfixa podem ser eficientemente avaliadas em máquinas
baseadas em pilhas, também conhecidas como máquinas de zero endereços. Nesse tipo de máquinas, operandos são
explicitamente introduzidos e retirados do topo da pilha por instruções push e pop, respectivamente. Além disso, a
aplicação de um operador retira do topo da pilha seus operandos e retorna ao topo da pilha o resultado de sua
aplicação.
Por exemplo, a avaliação da expressão a*(b+c) em uma máquina baseada em pilha poderia ser traduzida para o
código
 push a
 push b
 push c
Geração de código intermediário 26
 add
 mult
Otimização de código
A etapa final na geração de código pelo compilador é a fase de otimização. Como o código gerado através da
tradução orientada pela sintaxe contempla expressões independentes, diversas situações contendo seqüências de
código ineficiente podem ocorrer. O objeto da etapa de otimização de código é aplicar um conjunto de heurísticas
para detectar tais seqüências e substituí-las por outras que removam as situações de ineficiência.
As técnicas de otimização que são usadas em compiladores devem, além de manter o significado do programa
original, ser capazes de capturar a maior parte das possibilidades de melhoria do código dentro de limites razoáveis
de esforço gasto para tal fim. Em geral, os compiladores usualmente permitem especificar qual o grau de esforço
desejado no processo de otimização. Por exemplo, em gcc há opções na forma -O... que dão essa indicação, desde
O0 (nenhuma otimização) até -O3 (máxima otimização, aumentando o tempo de compilação), incluindo também
uma opção -Os, indicando que o objetivo é reduzir a ocupação de espaço em memória.
Algumas heurísticas de otimização são sempre aplicadas pelos compiladores. Por exemplo, se a concatenação de
código gerado por duas expressões no programa original gerou uma situação de desvio incondicional para a linha
seguinte, como em
(a)
 goto _L1
_L1: (b)
esse código pode ser seguramente reduzido com a aplicação da técnica de eliminação de desvios desnecessários,
resultando em
 (a)
_L1: (b)
Outra estratégia de otimização elimina os rótulos não referenciados por outras instruções do programa. Assim, se o
rótulo _L1 estivesse sendo referenciado exclusivamente por essa instrução de desvio, ele poderia ser eliminado em
uma próxima aplicação das estratégias de otimização.
As técnicas de otimização podem ser classificadas como independentes de máquina, quando podem ser aplicadas
antes da geração do código na linguagem assembly, ou dependentes de máquina, quando aplicadas na geração do
código assembly.
A otimização independente de máquina tem como requisito o levantamento dos blocos de comandos que compõem o
programa. Essa etapa da otimização é conhecida como a análise de fluxo, que por sua vez contempla a análise de
fluxo de controle e a análise de fluxo de dados. Estratégias que podem ser aplicadas, analisando um único bloco de
comandos são denominadas estratégias de otimização local, enquanto aquelas que envolvem a análise simultânea de
dois ou mais blocos são denominadas estratégias de otimização global.
Algumas estratégias básicas de otimização, além da já apresentada eliminação de desvios desnecessários, são
apresentadas a seguir.
A estratégia de eliminação de código redundante busca detectar situações onde a tradução de duas expressões gera
instruções cuja execução repetida não tem efeito. Por exemplo, em
x := y
 ...
Otimização de código 27
 x := y
se não há nenhuma mudança no valor de y entre as duas instruções, então a segunda instrução poderia ser eliminada.
O mesmo aconteceria se a segunda instrução fosse
y := x
e o valor de x não fosse alterado entre as duas instruções.
Outra estratégia básica é a eliminação de código não-alcançável, ou “código morto”. Por exemplo, se a seguinte
seqüência de código fosse gerada por um conjunto de expressões
...
 goto _L1
 x := y
_L1: ...
a instrução contendo a atribuição de y a x nunca poderia ser executada, pois é precedida de um desvio incondicional
e não é o destino de nenhum desvio, pois não contém um rótulo na linha. Assim, essa linha poderia ser eliminada e
provocar posteriormente a aplicação da estratégia de eliminação de desvios desnecessários.
O uso de propriedades algébricas é outra estratégia de otimização usualmente aplicada. Nesse caso, quando o
compilador identifica que uma expressão aritmética foi reduzida a x=0 ou 0=x ou x-0 ou x*1 ou 1*x ou x/1, então
ele substitui a expressão simplesmente por x. Outra classe de propriedades algébricas que é utilizada tem por
objetivo substituir operações de alto custo de execução por operações mais simples, como
2.0*x = x+x
x2 = x*x
x/2 = 0.5*x
Particularmente no último caso, se x for uma variável inteira a divisão por dois pode ser substituída por um
deslocamento da representação binária à direita de um bit. Genericamente, a divisão inteira por 2n equivale ao
deslocamento à direita de n bits na representação binária do dividendo. Da mesma forma, a multiplicação inteira por
potências de dois pode ser substituída por deslocamento de bits à esquerda.
A utilização de propriedades algébricas permite também o reuso de subexpressões já computadas. Por exemplo, a
traduçãodireta das expressões
 a = b + c;
 e = c + d + b;
geraria o seguinte código intermediário:
 a := b + c
 _t1 := c + d
 e := _t1 + b
No entanto, o uso da comutatividade e associatividade da adição permite que o código gerado seja reduzido usando a
eliminação de expressões comuns, resultando em
 a := b + c
 e := a + d
Diversas oportunidades de otimização estão associadas à análise de comandos iterativos. Uma estratégia é a
movimentação de código, aplicado quando um cálculo realizado dentro do laço na verdade envolve valores
invariantes na iteração. Por exemplo, o comando
Otimização de código 28
 while (i < 10*j) {
 a[i] = i + 2*j;
 ++i;
 }
estaria gerando um código intermediário equivalente a
 _L1: _t1 := 10 * j
 if i >= _t1 goto _L2
 _t2 := 2 * j
 a[i] := i + _t2
 i := i + 1
 goto _L1
 _L2: ...
No entanto, a análise de fluxo de dados mostra que o valor de j não varia entre iterações. Assim, o compilador
poderia mover as expressões que envolvem exclusivamente constantes na iteração para fora do laço, substituindo
esse código por
_t1 := 10 * j
 _t2 := 2 * j
 _L1: if i >= _t1 goto _L2
 a[i] := i + _t2
 i := i + 1
 goto _L1
 _L2: ...
Um exemplo de otimização dependente de máquina pode ser apresentado para a expressão
 x := y + K
onde K é alguma constante inteira. Na tradução para o código assembly de um processador da família 68K, a forma
genérica poderia ser algo como
 MOVE.L y,D0
 ADDI.L #K,D0
 MOVE.L D0,x
No entanto, se o compilador verificasse que o valor de K fosse uma constante entre 1 e 8, então a segunda instrução
assembly poderia ser substituída por ADDQ.L, que utilizaria em sua codificação apenas dois bytes ao invés dos seis
bytes que seriam requeridos pela instrução ADDI.L.
Nomes dos colaboradores 29
Nomes dos colaboradores
Nome dos colaboradores deste wikilivro
Lsd76
Fontes e Editores da Página 30
Fontes e Editores da Página
Capa  Fonte: http://pt.wikibooks.org/w/index.php?oldid=137915  Contribuidores: Jorge Morais, Lsd76, Marcos Antônio Nunes de Moura, 2 edições anónimas
O que é um compilador  Fonte: http://pt.wikibooks.org/w/index.php?oldid=204283  Contribuidores: Jorge Morais, Thiago, 4 edições anónimas
Fases de um compilador  Fonte: http://pt.wikibooks.org/w/index.php?oldid=204280  Contribuidores: Jorge Morais, Thiago, 2 edições anónimas
Teoria das linguagens formais  Fonte: http://pt.wikibooks.org/w/index.php?oldid=244176  Contribuidores: Abacaxi
Linguagens regulares e autômatos finitos  Fonte: http://pt.wikibooks.org/w/index.php?oldid=247646  Contribuidores: Abacaxi, 1 edições anónimas
Linguagens livres de contexto  Fonte: http://pt.wikibooks.org/w/index.php?oldid=244177  Contribuidores: Abacaxi
Análise léxica  Fonte: http://pt.wikibooks.org/w/index.php?oldid=265322  Contribuidores: Abacaxi, 1 edições anónimas
Análise sintática  Fonte: http://pt.wikibooks.org/w/index.php?oldid=244172  Contribuidores: Abacaxi
Tradução dirigida por sintaxe  Fonte: http://pt.wikibooks.org/w/index.php?oldid=244175  Contribuidores: Abacaxi
Análise semântica  Fonte: http://pt.wikibooks.org/w/index.php?oldid=244167  Contribuidores: Abacaxi
Geração de código intermediário  Fonte: http://pt.wikibooks.org/w/index.php?oldid=244171  Contribuidores: Abacaxi
Otimização de código  Fonte: http://pt.wikibooks.org/w/index.php?oldid=244170  Contribuidores: Abacaxi
Nomes dos colaboradores  Fonte: http://pt.wikibooks.org/w/index.php?oldid=204282  Contribuidores: Jorge Morais, Lsd76, Thiago, 1 edições anónimas
Fontes, Licenças e Editores da Imagem 31
Fontes, Licenças e Editores da Imagem
Image:Nt-compilador.png  Fonte: http://pt.wikibooks.org/w/index.php?title=Ficheiro:Nt-compilador.png  Licença: Creative Commons Attribution-Sharealike 2.5  Contribuidores: User:Nuno
Tavares
Image:Nuvola apps konsole.png  Fonte: http://pt.wikibooks.org/w/index.php?title=Ficheiro:Nuvola_apps_konsole.png  Licença: GNU Lesser General Public License  Contribuidores: Alno,
Alphax
Imagem:Parsing-example.png  Fonte: http://pt.wikibooks.org/w/index.php?title=Ficheiro:Parsing-example.png  Licença: GNU Free Documentation License  Contribuidores:
en:User:Nathanfulk
Imagem:Rekopis chopin.jpg  Fonte: http://pt.wikibooks.org/w/index.php?title=Ficheiro:Rekopis_chopin.jpg  Licença: Public Domain  Contribuidores: Andreagrossmann, Kevyn, Maire, Man
vyi, Niki K, Subitosera, Tsca
Licença 32
Licença
Creative Commons Attribution-Share Alike 3.0
//creativecommons.org/licenses/by-sa/3.0/

Outros materiais