You are on page 1of 240

Prof. Dr.

Wagner José Dizeró
wagner@unilins.edu.br

Análise léxica (scanner). Análise sintática (parser). Análise Semântica. Recuperação de erros. Aspectos e ferramentas para construção de compiladores. Sintaxe abstrata. Análise de escopo checagem de tipos. Registros de ativação. Tradução para código intermediário. Geração de código. Otimização.

Livro Texto:
1.

Implementação de Linguagens de Programação: Compiladores; Toscani, Simão S. & Price, Ana
Maria de Alencar; Ed. Bookman; 3ª edição; 2008; 5 exemplares. 2007; 5 exemplares.

2.

Compiladores: Princípios, Técnicas, e Ferramentas; Aho, Alfred et al.; Ed. Pearson; 2ª edição; Compiladores: Princípios e Práticas; Louden, Kenneth C.; Ed. Thomson Pioneira; 1ª edição;

3.

2004; 5 exemplares.

Bibliografia Complementar:
4.

Linguagens Formais: Teoria, Modelagem e Implementação; Ramos, Marcus V. M., Neto, João José; & Veja, Ítalo Santiago; Ed. Bookman; 1ª edição; 2009; 10 exemplares. Fundamentos Matemáticos para a Ciência da Computação; Gersting, Judith L.; Ed. LTC; 5ª
edição; 2004; 17 exemplares. exemplares.

5.

6.

Linguagens Formais e Autômatos; Menezes, Paulo F. B.; Ed. Sagra Luzzato; 4ª edição; 2002; 9 Elements of the Theory of Computation; Lewis, Harry R.; Ed. Prentice-Hall; 2ª edição; 1998; 1 Introduction to Formal Language Theory; Harrison, Michael A.; Ed. Addison-Wesley; 1ª edição;

7.

exemplar.
8.

1978; 1 exemplar.

Linguagens de Programação Tradutores
◦ Compilador ◦ Interpretador

Fases de um compilador Análise
◦ Analisador Léxico ◦ Analisador Sintático ◦ Analisador Semântico

Síntese
◦ Geração de código intermediário ◦ Otimização de código ◦ Geração de código objeto

Linguagens de programação são notações para se descrever algoritmos computacionais. Mas. conforme os conhecemos hoje. . um programa primeiro precisa ser traduzido para um formato que lhe permita ser executado pelo computador (linguagem de máquina). Para diminuir as diferenças entre a linguagem de máquina e a linguagem natural é que surgiu a linguagem de programação. antes que possa rodar. Os computadores. pois todo software é escrito em alguma linguagem de programação. dependem de linguagens de programação.

Através das linguagens de programação. consegue-se: Facilidades para se escrever programas Diminuição do problema de diferenças entre máquina Facilidade de depuração e manutenção de programas Melhoria da interface homem/máquina Redução no custo e tempo necessário para o desenvolvimento de programas Aumento da compatibilidade e modularidade .

Tradutores são programas capazes de converter um código geralmente de alto nível para um código que o computador interprete. um tradutor tem a função de traduzir uma linguagem abstrata para uma linguagem binária. Em geral. Em outras palavras. os tradutores são programas bastante complexos. Há 2 tipos principais de tradutores: ◦ Compiladores ◦ Interpretadores .

e a linguagem-alvo é um códigocódigo-objeto (código de máquina ou programa executável). a linguagem-fonte (código códigocódigo-fonte) fonte é uma linguagem de alto nível. C++ . Compilador é um dos tipos de tradutor mais utilizados. Exemplo: Pascal. alvo Geralmente. C.Um compilador é um programa que recebe como entrada um programa escrito na linguagemlinguagem-fonte e produz um programa equivalente na linguagemlinguagem-alvo. como C ou Java.

a possibilidade de não poder visualizar o código-fonte pode ser uma desvantagem ◦ Processo de correção ou alteração do código requer que ele seja novamente recompilado .Vantagens: ◦ O código compilado é mais rápido ◦ Impossibilita (ou pelo menos dificulta) ser quebrado e visualizado o código-fonte original ◦ Permite otimização do código por parte do compilador ◦ Compila o código somente se estiver sem nenhum erro Desvantagens: ◦ Para ser utilizado o código precisa passar por muitos níveis de compilação ◦ Assim como vantagem.

Cada execução do programa precisa ser novamente traduzido e interpretado. Ele traduz o programa linha a linha. e o programa vai sendo utilizado na medida em que vai sendo traduzido. ao contrário do compilador.O interpretador. roda o código-fonte escrito como sendo o código objeto. VB . Exemplo: PHP.

Vantagens: ◦ Correções e alterações são mais rápidas de serem realizadas ◦ Código não precisa ser compilado para ser executado ◦ Consomem menos memória Desvantagens: ◦ A execução do programa é mais lenta ◦ Necessita sempre ser lido o código original para ser executado .

.

cuja finalidade é converter um programa PF. Basicamente. o qual será executado em uma máquina M. um compilador é um programa de computador escrito em uma linguagem L. Em outras palavras: “é um programa usado para gerar programas”. para um programa-objeto PO. denominado programa-fonte. .Programa que transforma uma linguagem de alto nível em linguagem de máquina.

Programa Fonte COMPILADOR Erros Programa Objeto .

sistemas operacionais e algoritmos. como por exemplo: . Algumas técnicas básicas de construção de compiladores podem ser usadas na construção de ferramentas variadas para o processamento de linguagens.A construção de compiladores engloba várias áreas desde teoria de linguagens de programação até engenharia de software. passando por arquitetura de máquina.

São exemplos de textos formatados os documentos escritos em editores convencionais. etc.Compiladores para linguagens de programação: ◦ um compilador traduz um programa escrito numa linguagem fonte em um programa escrito em uma linguagem objeto. figuras. itálico. Interpretadores de queries (consultas a banco de dados): ◦ um interpretador de queries traduz uma query composta por operadores lógicos ou relacionais em comandos para percorrer um banco de dados. fórmulas matemáticas. etc). os documentos escritos em Latex. Formatadores de texto: ◦ um formatador de texto manipula um conjunto de caracteres composto pelo documento a ser formatado e por comandos de formatação (parágrafos. os documentos escritos em HTML (HyperText Markup Language). negrito.. .

.

. compõem-se de funções padronizadas.Independente da linguagem a ser traduzida ou do programa objeto a ser gerado. que compreendem a análise do programa fonte e a posterior síntese para a derivação do código objeto. os compiladores. de um modo geral.

Programa Fonte COMPILADOR ANÁLISE TABELAS Representação Intermediária Erros SÍNTESE Programa Objeto .

Analisador Léxico ANÁLISE Analisador Sintático Analisador Semântico Gerador de código intermediário Otimizador de código Gerador de Código-objeto Tratamento de Erros SÍNTESE Tabela de Símbolos .

.Passagem completa do programa compilador sobre o programa fonte que está sendo compilado.Procedimento que realiza uma função bem definida no processo de compilação. Na prática. Fase .O processo de compilação é comumente estruturado em fases. sob o ponto de vista de implementação. Passo . podem não estar separados em módulos específicos.

Velocidade ◦ possível vantagem para um passo Espaço ◦ possível vantagem para um passo (dados x programa) Modularidade ◦ vantagem de múltiplos passos Flexibilidade ◦ vantagem de múltiplos passos Transformações/otimizações de programas ◦ vantagem de múltiplos passos .

caractere a caractere. e desprezando comentários e brancos desnecessários. . o texto fonte. identificando tokens. identificadores. etc. O analisador léxico lê.O objetivo principal desta fase é identificar seqüências de caracteres que constituem unidades léxicas (tokens). verificando se os caracteres lidos pertencem ao alfabeto da linguagem. delimitadores. Os tokens constituem classes de símbolos tais como palavras reservadas.

mas somente 7 tokens. temos: id 1 atrib := id 2 op + id 3 op * cte 60 TABELA DE SÍMBOLOS posicao inicial taxa 1 2 3 .Por exemplo. Esse código contém 23 caracteres diferentes de espaço. Após a análise léxica. considere a linha de código: posicao := inicial + taxa * 60 Cada token é composto por um ou mais caracteres.

que mostra a estrutura gramatical da seqüências de tokens. O analisador sintático utiliza os tokens produzidos pelo analisador léxico para criar uma representação intermediária tipo árvore. := <id.1> <id.A segunda fase do compilador é a análise sintática.2> <id.3> + * 60 .

Uma parte importante da análise semântica é a verificação de tipo.O analisador semântico utiliza a árvore de sintaxe e as informações da tabela de símbolos para verificar a consistência semântica do programa fonte.1> <id. := <id.2> <id.3> + * inttofloat 60 . em que o compilador verifica se cada operador possui operandos compatíveis.

que podemos imaginar como uma representação para uma máquina abstrata. muitos compiladores geram uma representação intermediária explícita de baixo nível.Depois das análises sintática e semântica do programa fonte. t1 = inttofloat(60) t2 = id3 * t1 t3 = id2 + t2 id1 = t3 .

0 id1 = id2 + t1 . faz algumas transformações no código intermediário com o objetivo de produzir um código objeto melhor. mas também pode ser desejado um código menor ou que consuma menos energia. t1 = id3 * 60. Normalmente.A fase de otimização de código. independente da arquitetura de máquina. melhor significa mais rápido.

o código intermediário poderia ser traduzido para o seguinte código de máquina: LDF MULF LDF ADDF STF R2. id2 R1.0 R2 . de acordo com a arquitetura destinada. id1. Por exemplo. R1 #60. usando os registradores R1 e R2. R2. R1. id3 R2.O gerador de código recebe como entrada uma representação intermediária de programa fonte e o mapeia em uma linguagem objeto. R1.

pode-se armazenar informações sobre os argumentos. seu tipo.Uma função essencial de um compilador é registrar os nomes de variáveis usados no programa fonte e coletar informações sobre os diversos atributos de cada nome. A tabela de símbolos deve ser projetada como uma estrutura de dados que permita ao compilador encontrar rapidamente o registro para cada nome. No caso de funções. o tipo retornado e o método de passagem de parâmetro. Esses atributos podem prover informações sobre o espaço de memória alocado para um nome. seu escopo. .

têm melhor desempenho. mas exigem mais memória e esforço de programação. Tabelas hash . mas seu desempenho é pobre quando o número de consultas é elevado. 2. Árvores binárias 3. os mais comuns: 1.Existem vários modos de se organizar e acessar as tabelas de símbolos sendo. .é o mecanismo mais simples. Listas lineares .

ainda que erros tenham sido detectados.Esse módulo de por objetivo tratar os erros que são detectados em todas as fases de análise de programa fonte. Cada fase de um compilador requer um tratamento de erros ligeiramente diferente. podem ocorrer erros estáticos (em tempo de compilação) e erros de execução. Num programa. Qualquer fase analítica deve prosseguir. .

. ou pelo menos tentar isolá-lo “recuperando” o resto do texto Detectar o erro. o mais cedo possível ◦ não propagar o erro à fase seguinte ◦ assegurar uma perda mínima de texto ◦ confiança máxima na “correção” efetuada Evitar “mensagens em cascata” ◦ erros assinalados em partes corretas do programa Não degradar a eficiência ◦ tempo gasto na análise de texto correto Procurar soluções genéricas ◦ soluções que possam ser descritas formalmente de modo a serem geradas automaticamente.Poupar esforço ao programador ◦ enviar mensagens claras e completas ◦ tentar “corrigir o erro”.

… Erros sintáticos ◦ sequências inválidas de símbolos.. incompatibilidade de tipos. EOF durante o reconhecimento de um símbolo. expressão aritmética com parênteses não balanceados.Erros léxicos ◦ erro de grafia. … Erros semânticos ◦ identificadores não declarados ou redeclarados. … Erros lógicos ◦ divisão por zero. caracteres não previstos.. chamada infinitamente recursiva. utilização fora do seu escopo. sequências inválidas. . .

Erro fatal ◦ impossível continuar a análise Erro grave ◦ continua a análise. . mas é impossível gerar código Aviso ◦ a análise e geração continuam. mas foi feita uma “correção”.

classe. posição. 2 : escrever (‘ERRO GRAVE ’).linha. 2 : escrever (‘NA ANÁLISE SINTÁCTICA ‘). símbolo. caso código seja 1:… 2:… … fim .coluna). posição. 3 : escrever (‘AVISO ’). caso classe seja 1 : escrever (‘NA ANÁLISE LÉXICA ‘). ‘NA POSIÇÃO ‘. escrever (‘NO SÍMBOLO ‘. símbolo. posição) caso tipo seja 1 : escrever (‘ERRO FATAL ’). código.mensagem_erro (tipo. 3 : escrever (‘NA ANÁLISE SEMÂNTICA ‘).

Linguagem Natural Linguagens com Estrutura de Frases Analisador Semântico Linguagens Sensíveis ao Contexto Analisador Sintático Linguagens Livre de Contexto Analisador Léxico Linguagens Regulares .

1. 3. Aponte as vantagens e desvantagens dos interpretadores em relação aos compiladores. 2. 4. Explique o processo de compilação: fases e interrelacionamento. 5. Qual o significado de “passo” no processo de compilação? Quais as vantagens e desvantagens de implementar um compilador em vários passos? Qual a função da tabela de símbolos? Como ela é utilizada nas fases do compilador? Aponte alguns tipos de erros que podem ocorrer em cada fase de análise de um compilador. .

Léxica. Sintática e Semântica .

A análise tem como objetivo entender o código fonte e representá-lo em uma estrutura intermediária. 2. A análise sintática é o processo de se determinar se uma cadeia de tokens pode ser gerada por uma gramática. buscando a separação e identificação dos elementos componentes do programa fonte. A análise semântica assegura que as regras sensíveis ao contexto da linguagem estejam verificadas quanto à sua validade. 3. caracter a caracter. É dividida em 3 partes: 1. A função da análise léxica é ler o código fonte. denominados símbolos léxicos ou tokens. .

.

Analisador Léxico ANÁLISE Analisador Sintático Analisador Semântico Gerador de código intermediário Otimizador de código Gerador de Código-objeto Tratamento de Erros SÍNTESE Tabela de Símbolos .

Função de um Analisador Léxico (AL) Tarefas secundárias Vantagens da Separação entre AL e Sintática Erros Léxicos Especificação e Reconhecimento dos tokens Implementação de um AL .

diz-se que ela compõe um “token léxico”. . Por fim.A tarefa do analisador léxico é avaliar um código fonte e verificar se uma série de símbolos é válida ou não. produzir como saída uma seqüência de tokens com seus respectivos códigos que o Analisador Sintático usará para validar regras da gramática. Quando uma palavra é reconhecida como válida.

Consumir comentários e separadores ◦ Espaço em branco. . que não fazem parte da linguagem. tabulação e CR LF. Processar diretivas de controle Relacionar as mensagens de erros do compilador com o programa-fonte ◦ Manter a contagem dos CR LF’s e passar esse contador junto com a posição na linha para a rotina que imprime erros Manipular a Tabela de Símbolos para inserir os identificadores.

Simplificação ◦ Um AS que tenha que fazer o tratamento de comentários e separadores é bem mais complexo do que um que assume que eles já foram removidos Eficiência ◦ Uma parte apreciável do tempo de compilação corresponde à AL que separada facilita a introdução de certas otimizações Manutenção ◦ Toda parte referente à representação dos terminais está concentrada numa única rotina tornando mais simples as modificações da representação .

◦ O AL devolve o código de identificador e deixa para as próximas fases identificar os erros Tratamento de Constantes ◦ ◦ ◦ ◦ Número de casas decimais para reais String não finalizada Tamanho máximo dos identificadores Fim de arquivo inesperado .Poucos erros são discerníveis no nível léxico ◦ O AL tem uma visão muito localizada do programa-fonte Exemplo: fi (a > b) then ◦ O AL não consegue dizer que fi é a palavra reservada if mal escrita ou o uma função sem .

e as constantes de todos os tipos Por exemplo: ◦ reconhecer “while” como uma palavra reservada ◦ reconhecer “1234” como um número inteiro ◦ identificar “3%%26*&” como uma seqüência inválida . palavras-reservadas. para se decidir por um código. é preciso ler um caractere a mais.É preciso percorrer o código-fonte caractere por caractere e concatená-los para formar o token. símbolos especiais simples e compostos. Às vezes. o qual deve ser devolvido a cadeia de entrada (lookahead) Tipos de Tokens: ◦ ◦ ◦ ◦ identificadores.

Pode ser representado por: Gramática Expressão regular Autômato .

T ∩ N = ∅ P é um conjunto de regras de produção S é o símbolo inicial da gramática (S ∈ N) .Uma gramática G é um mecanismo para gerar as sentenças (ou palavras) de uma linguagem e é definida pela quádrupla: (N. P. T. S) onde: ◦ ◦ ◦ ◦ N é um conjunto de símbolos não terminais T é um conjunto de símbolos terminais.

R } T = { l. T. I ) N = { I.Exemplo para geração de identificadores que iniciam por letra (l). d } P = { I → l | l R. podendo ser seguida por qualquer número de letras e/ou dígitos. P. G = ( N. R → l R | d R | l | d } .

ou seja. isto é. 2. ou seja { } ε é a ER que representa a linguagem cuja única palavra é a palavra vazia. como segue: 1. onde x ∈ ∑. { x } se r₁ e r₂ são ER que representam as linguagens L(r₁) e L(r₂): (r₁ + r₂) é a ER que representa a linguagem L(r₁) ∪ L(r₂) (r₁ r₂) é a ER que representa a linguagem { vw | v ∈ L(r₁) e w ∈ L(r₂) }. ∅ é a ER que representa a linguagem vazia. ou seja. a concatenação de uma palavra de L(r₁) com uma palavra de L(r₂).Uma expressão regular (ER) sobre o alfabeto ∑ é definida indutivamente a partir de expressões regulares básicas.Epsilon . 3. ε . nesta ordem (r₁)* é a ER que representa a linguagem L*(r₁). 4. ou seja { ε } x. é a ER a linguagem cuja única palavra é x. o conjunto de palavras que podem ser formadas com zero ou mais letras de ∑.

seguido por zero ou mais a conjunto dos inteiros (assumindo que d representa dígitos) conjunto de identificadores que iniciam por uma letra seguida opcionalmente por letras e dígitos todas as palavras sobre {a.b} todas as palavras contendo aa como subpalavra todas as palavras contendo exatamente dois b todas as palavras que terminam com aa ou bb todas as palavras que não possuem dois a consecutivos .aa ba* d(d)* l(l+d)* (a+b)* (a+b)*aa(a+b)* a*ba*ba* (a+b)*(aa+bb) (a+ε)(b+ba)* somente a palavra aa todas as palavras que iniciam por b.

∑ . e₁.Um autômato finito M sobre um alfabeto ∑ é uma 5-tupla (K.Sigma maiúsculo δ . onde: ◦ K é um conjunto finito de estados ◦ ∑ é o alfabeto dos símbolos da linguagem ◦ δ : K x ∑ → K é a função de transição de estados ◦ e₁ é o estado inicial ◦ F é o conjunto de estados finais. F).Delta minúsculo . δ. ∑.

. e₄ ) δ(e₁. ) = e₃ δ(e₃. . d) = e₂ δ(e₂. e₃. e₂.M = ( K. ) K = ( e₁. e₄ ) F = (e₂. F ) ∑ = ( d. e₁. δ. ∑. d) = e₄ δ(e₄. d) = e₄ . d) = e₂ δ(e₂.

M = ( K. e₄ ) F = (e₂. e₄ ) TABELA DE TRANSIÇÃO d e₁ e₂ e₃ e₄ e₂ e₂ e₄ e₄ . F ) ∑ = ( d. δ. ) K = ( e₁. ∑. . e₀. e₃. e₃ . e₂.

e₃ d d e₄ .d d e₁ e₂ .

. *) Elaborar uma expressão regular para representar o formato de RG Elaborar uma expressão regular para representar o formato de e-mail 2.1.. . Elaborar uma gramática para representar os números reais.. “ Elaborar um autômato para representar comentários no formato (* .. 5. 3. Elaborar um autômato para representar o tipo literal (string) no formato “ . 4.

entre elas. temos: ◦ Elaborar um código que simula o funcionamento do autômato finito ◦ Utilizar geradores. que será usada pelo analisador sintático. .Há diferentes maneiras de se implementar um analisador léxico. como por exemplo. incrementaremos nosso código para gerar uma tabela de símbolos. LEX Começaremos criando alguns algoritmos para reconhecer tokens. Posteriormente.

e₀. δ. ∑. F ) ∑ = ( l.M = ( K. e₂ ) F = ( e₂ ) TABELA DE TRANSIÇÃO l e₁ e₂ e₂ e₂ d e₂ . d ) K = ( e₁.

digit: set of(0. .l.Z). case state do 1: if charRead in letter state := 2 else state := 3 //erro 2: if charRead in (letter OR digit) state := 2 //desnecessário else state := 3 //erro end case end while if state = 2 return true else return false end.. while NOT EOF input do begin charRead = nextChar(input). d e₁ l e₂ letter: set of (a. begin read(input) state := 1..9).

int main() { char input[256]. gets(input). return 0. else printf("Inválido"). case 2: if(isalpha(chRead) || isdigit(chRead)) state = 2. printf("Identificador: "). case 3: exit. break. state = 1. break. } .#include <iostream> int isIdentifier(char *). switch(state){ case 1: if(isalpha(chRead)) state = 2. } } return state==2. else state = 3. //desnecessário else state = 3. int lineSize. lineSize = strlen(input). if(isIdentifier(input)) printf("Válido"). for(int column=0. column++){ chRead = input[column]. break. } int isIdentifier(char *input) { char chRead. column<lineSize. getchar().

digit: set of(0.. else if charRead in letter state := 2 else state := 3 //erro ..9)...9 ..Z). d e₁ l e₂ letter: set of (a. Onde: b: espaço em branco l: letra a.Z d: dígito 0. while NOT EOF input do begin charRead = nextChar(input)..b l. case state do 1: if charRead is space while charRead is space do charRead = nextChar(input). begin read(input) state := 1.

state = 1.. case 1: if(isspace(chRead)) { column++.... } if(column<lineSize) column--. . . } else if(isalpha(chRead)) state = 2. break.. while(column<lineSize && isspace(input[column])){ column++. else state = 3.

5.. Elaborar um programa para reconhecer literais (string) no formato ‘ . um número.: antes de programar. *) Elaborar um programa para ler uma entrada e retornar se o token representa um identificador.1. 2... Elaborar um programa reconhecer números reais.. ‘ Elaborar um programa para reconhecer comentários no formato (* . Elaborar um programa reconhecer números inteiros. um literal. obs. um comentário ou um símbolo inválido. 4. 3. criar o autômato. .

d d e1 e3 e4 e5 e6 ( e7 !* !) * * ) e8 * e9 e10 e11 .l. d e2 b l d ‘ !’ ’ d .

Manipular arquivo ◦ O código-fonte é lido pelo analisador léxico a partir de um arquivo de texto (sem formatação) Separar tokens ◦ Cada token deve ser identificado e etiquetado com sua classe. valor e posição (linha e coluna) Alocar em fila ◦ Cada token deve ser armazenado numa fila (com alocação dinâmica de memória) .

. gets(physicalFile). system("cls"). char chRead. int lineSize. int main() { FILE *sourceCode.#include <iostream> #define MAXLINESIZE 256 typedef struct { unsigned short int line. printf("Informe o nome do arquivo: "). } Coordinate. char physicalFile[256]. char currentLine[MAXLINESIZE]. unsigned char column. Coordinate cFile.

printf("%c". cFile.column]. } cFile.column = 0.line = 1. lineSize = strlen( currentLine ). return 0. getchar().\n" ). exit( 1 ). ( MAXLINESIZE -2 ). } .line). sourceCode ).line++. while ( !feof( sourceCode ) ) { fgets( currentLine. "r" ) ) == NULL ) { printf( "O arquivo não pode ser aberto.column < lineSize. } fclose( sourceCode ). chRead).cFile.if (( sourceCode = fopen( physicalFile. cFile. printf("%3d: ". } cFile. for ( cFile.column++ ) { chRead = currentLine[cFile.

return 0. printf("Digite um identificador: "). void initToken(char *. lexicalScanner(input). char *). int lexicalScanner(char *). void addToken(char *. getchar(). char *). int main() { char input[256]. char).#include <iostream> void concat (char *. } . gets(input).

char ch) { int i = strlen(str). *(str + (i + 1)) = '\0'. } void initToken(char *token. initToken(token. } void addToken(char *token. *state = 1. *(str + i) = ch.void concat (char *str. char *state) { printf("-> %s\n". state).token). } ."\0"). char *state) { strcpy(token.

switch(state){ case 1: if(isspace(chRead)) { column++. lineSize = strlen(input). state = 1. } if(column<lineSize) column--. &state). for(int column=0. column++){ chRead = input[column]. while(column<lineSize && isspace(input[column])){ column++. column<=lineSize. char token[100].int lexicalScanner(char *input){ char chRead. initToken(token. } . state. int lineSize.

state = 2. break. } break. chRead . break. &state). column--. column--. } } return 0.else if(isalpha(chRead)){ concat(token. } else state = 3. } . state = 2. case 3: initToken(token. chRead). case 2: if(isalpha(chRead) || isdigit(chRead)) { concat(token. &state). } else { addToken(token.

} Cell.#include <iostream> typedef struct { int value. typedef struct TCell { Item item. } Queue. } Item. typedef struct TCell *Pointer. typedef struct { Pointer first. Pointer next. . last.

int newQueue(Queue *queue){ queue->first = (Pointer) malloc(sizeof(Cell)). queue->last->item = *item. return 0. } else { return 1. return 0. } } . queue->last = queue->first. } int queueEmpty(Queue *queue){ return (queue->first == queue->last). queue->last->next = NULL. Item *item){ if(queue->last->next = (Pointer) malloc(sizeof(Cell))) { queue->last = queue->last->next. } int push(Queue *queue. queue->first->next = NULL.

int pop(Queue *queue. aux = queue->first. free(aux). if(queueEmpty(queue)){ return 1. queue->first = queue->first->next. while(queue->first){ aux = queue->first. } } . } else { *item = queue->first->next->item. free(aux). queue->first = queue->first->next. Item *item){ Pointer aux. } } int freeQueue(Queue *queue){ Pointer aux. return 0.

} } char menu() { printf("\n1. fflush(stdin). while(aux != NULL){ printf("%d\n". } . printf("\nChoice: "). aux = aux->next. Quit"). Push"). Pop"). return 1. } return 0.int printQueue(Queue *queue){ if(queueEmpty(queue)){ printf("Empty Queue\n"). Print"). aux->item. } else { Pointer aux = queue->first->next. printf("\n0. printf("\n2.value). printf("\n3. return getchar().

} } while(op!='0'). &item) ? printf("Empty Queue\n") : printf("POP %d\n". &item) ? printf("Error\n") : printf("PUSH %d\n". freeQueue(&queue). switch(op){ case '1': printf("Enter a value: "). item.value). item. break. scanf("%d". do { op = menu().int main(){ char op. Queue queue.value). Item item. case '2': pop(&queue. break. break.value). &item. newQueue(&queue). } . case '3': printQueue(&queue). return 0. push(&queue.

. . . *. / Operadores lógicos :=. <=. =. <>. >= Delimitadores ( ) : . -.Elaborar um autômato finito capaz de representar o analisador léxico de um subconjunto da linguagem Pascal. >. <. ◦ Consumir espaços em branco no início da cadeia ◦ Reconhecer: Identificadores Números (inteiros e reais) Comentários Literais Operadores aritméticos +.

9 ] a: [ + . . Z ] d: [ 0 . . !’ e7 ’ e28 e2 d l l e6 l.l. d e27 e26 . d l d e4 d e5 e8 !) ou !* * * e13 ) e14 e22 Onde: l: [ a .* / ] b: espaço em branco Q: caractere não reconhecido . . e1 a e24 e23 = ) < > : e20 > e18 = = e21 e19 = e17 e16 e15 e11 * !* e12 ( { e9 d ‘ !} } e10 Q l e3 . e25 b .

string. not.coluna) ◦ Identificar as palavras reservadas: begin. end. valor. and e or. while.Baseado no autômato proposto. coordenada (linha. armazenar: nome. for. const. real. do. classe. then. ◦ Para cada token. var. integer. if. criar um analisador léxico que leia um arquivo de entrada e gere as tabelas de símbolos e de erros usando lista dinâmica. to. else. . program.

.

Analisador Léxico ANÁLISE Analisador Sintático Analisador Semântico Gerador de código intermediário Otimizador de código Gerador de Código-objeto Tratamento de Erros SÍNTESE Tabela de Símbolos .

Constitui a segunda fase de um compilador. Sua função é verificar se as construções usadas na programa estão gramaticalmente corretas. Fase Léxica Sintática Entrada Cadeia de Caracteres Cadeia de Tokens Saída Cadeia de Tokens Árvore Sintática . Cada linguagem de programação possui regras que descrevem a estrutura sintática dos programas.

recebe do analisador léxico a sequência de tokens que constitui a sentença s e produz como resultado uma árvore de derivação. ou emite uma mensagem de erro. Analisador Sintático Sequência de Tokens Árvore Sintática . também chamado de parser.O analisador (ou reconhecedor) sintático. caso contrário. se a sentença é válida.

. o objetivo do analisador sintático é verificar se a sentença s pertence à linguagem gerada por G. Dada uma gramática livre de contexto G e uma sentença (programa-fonte) s.Por exemplo: ◦ ◦ ◦ ◦ um programa é composto por blocos. . um comando é composto por expressões. uma expressão é formada por tokens .. um bloco é composto por comandos.

. mesmo que encontrem erros no texto fonte.A árvore de derivação pode ser construída explicitamente (representada através de uma estrutura de dados) ou ficar implícita nas chamadas das rotinas que aplicam as regras de produção durante o reconhecimento. É recomendado que os analisadores sintáticos sejam projetados de modo que possam prosseguir na análise até o fim do programa.

if E1 then C1 else if E2 then C2 else C3 comando if expr E1 then comando C1 if expr E2 else comando then comando C2 else comando C3 .

◦ Yacc (Yeat Another Compiler of Compilers) . Dado o formalismo da linguagem BNF. é possível construir um programa que cria automaticamente um parser para uma determinada sintaxe. as estruturas sintáticas válidas são especificadas através de uma gramática livre de contexto (GLC) ou pela notação BNF.Normalmente.

Há duas estratégias básicas para a AS: ◦ Estratégia TOP-DOWN ou DESCENDENTE ◦ Estratégia BOTTOM-UP ou REDUTIVA .

Os métodos de análise baseados na estratégia top-down (descendente) constroem a árvore de derivação a partir do símbolo inicial da gramática (raiz da árvore). fazendo a árvore crescer até atingir suas folhas. Principais tipos: ◦ Recursivo com retrocesso (backtracking) ◦ Recursivo preditivo ◦ Tabular preditivo .

.A análise redutiva de uma sentença (ou programa) pode ser vista como uma tentativa de construir uma árvore de derivação a partir das folhas. Quando isso ocorre. esse lado direito é substituído (reduzido) pelo símbolo do lado esquerdo da produção. O processo de reconhecimento consiste em transferir símbolos da fita de entrada para a pilha até que se tenha na pilha um lado direito de produção.

Analisadores Sintáticos Descendente (Top-donw) Ascendente (Bottom-up) Recursivo com retrocesso (backtracking) Recursivo sem retrocesso (preditive) Preditiva Tabular (não recursivo) .

O standard ISO 14977 define uma extensão ao BNF designado EBNF e no qual existem quatro novos operadores: ◦ | ◦ [] ◦ {} escolha múltipla símbolos opcionais (zero ou uma vez) símbolos opcionais com repetição (zero ou mais vezes) ◦ { }+ símbolos com repetição (uma ou mais vezes) .

.Expressões constituídas por dígitos e sinais de mais e menos podem ser descritas através da gramática: <expr> ::= <num> | <num> <oper> <expr> <num> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 <oper> ::= + | - Uma cadeia de linguagem. pode ser exibida através de uma árvore sintática. gerada por uma gramática livre de contexto.

Por exemplo. a árvore gramatical para "9-5+2" é: .

.|X|Y|Z ..Começaremos com uma expressão simplificada: <EXPRESSAO> ::= <TERMO> | <TERMO> <OPERADOR> <EXPRESSAO> <OPERADOR> <TERMO> <NUMERO> <IDENT> <DIGITO> <LETRA> ::= + | ::= <NUMERO> | <IDENT> ::= {<DIGITO>}+ ::= <LETRA> {<LETRA> | <DIGITO>} ::= 0|1|2|3|4|5|6|7|8|9 ::= a|b|c|d|.

Podemos interpretar a regra como: "expressão é um número seguido de zero ou mais expressões conectadas por um operador“. Assim como: expressão = numero expressão = numero + expressão expressão = numero + expressão + expressão e assim por diante. .

} } . } } Procedimento TERMO() { se símbolo_lido = NÚMERO então { /* trate adequadamente um número */ obter_token(). } Procedimento EXPR() { TERMO(). se token = '+' ou token = '-' então { obter_token(). EXPR(). } senao se símbolo_lido = IDENT então { /* trate o identificador */ obter_token().Procedimento analisador_sintatico { obter_token(). /* chama o léxico */ EXPR().

Este problema pode ser comparado ao dos autômatos não‐determinísticos. pois ela não especifica com precisão a estrutura sintática do um programa.É uma gramática que permite construir mais de uma árvore de derivação para uma mesma sentença. Isto representa um problema para o analisador sintático. onde dois caminhos podem aceitar a mesma cadeia. .

(Sem alterar a gramática) ◦ Alterar a gramática para forçar a construção da árvore sintática correta. removendo a ambiguidade. Existem duas maneiras: ◦ Estabelecer uma regra que especifique a cada caso ambíguo qual é o caminho correto.A eliminação de ambiguidade não é tão simples como a eliminação de não‐determinismo em autômatos. .

expr -> expr op expr | ( expr ) | id | num op -> + | .| * | / expr expr expr op + expr num expr num op * expr num expr expr num op * expr num op + expr num .

direta‐esquerda). . Operadores com maior precedência são avaliados primeiro. Eles especificam uma ordem na avaliação dos operadores. Operadores com igual precedência são avaliados de acordo com a associatividade (esquerda‐direita.Para tratar o problema de ambiguidade em gramática são utilizados os conceitos de precedência e associatividade.

. introduz‐se um não terminal e uma regra gramatical Para tratar a associatividade dos operadores: ◦ Cria‐se regras gramaticais que serão recursivas à direita ou à esquerda.Para tratar a precedência dos operadores: ◦ Divide‐se os operadores em grupos de igual precedência ◦ Para cada nível de precedência.

Gramática para expressões sem ambiguidade: expr -> expr addop term | term term -> term mulop fator | fator fator -> ( expr ) | id | num addop -> + | mulop -> * | / term expr addop + term num fator num mulop * expr num .

Porém. seu projeto e sua implementação seriam muito simplificados. espera-se que um compilador auxilie o programador na localização e rastreamento de erros que inevitavelmente surgem nos programas. . chaves extras ou faltando.Se um compilador tivesse que processar apenas programas corretos. Erros sintáticos incluem ponto-e-vírgulas mal colocados. instruções incompletas ou expressões inválidas.

. No mínimo. ◦ Recuperar-se de cada erro com rapidez suficiente para detectar erros subsequentes. mas desafiadores em sua implementação: ◦ Informar a presença de erros de forma clara e precisa.O recuperador de erros de um analisador sintático possui objetivos simples. pois existe uma grande chance de que o local exato do erros seja em um dos tokens anteriores. é preciso informar o local no programa fonte onde o erro foi detectado. ◦ Acrescentar um custo mínimo no processamento de programas corretos.

Se os erros se acumularem. é melhor o compilador desistir depois de ultrapassar algum limite de erros do que produzir uma incomoda avalanche de ‘falsos’ erros.A técnica mais simples é interromper a análise sintática e emitir uma mensagem de erro informativa assim que o primeiro erro for detectado. Principais estratégias de recuperação de erros: ◦ ◦ ◦ ◦ Modo pânico Nível de frase Produções de erro Correção global .

ele tem a vantagem da simplicidade. ao detectar um erro. o analisador sintático descarta um símbolo de entrada de cada vez até que um conjunto de tokens de sincronismo seja encontrado (normalmente delimitadores. além da garantia de não entrar em um loop infinito. como ponto-evírgula). .Com esse método. Embora o modo pânico normalmente ignore um quantidade considerável de símbolos.

o analisador sintático pode realizar a correção local sobre o restante da entrada. É preciso ter cuidado para que as substituições não provoquem loops infinitos. pois pode corrigir qualquer cadeia de entrada.Ao detectar um erro. Tem sido usada em diversos compiladores com recuperação de erros. de maneira a ser possível continuar a análise. . A sua principal desvantagem é a dificuldade de lidar com situações em que o erro real ocorreu antes do ponto de detecção.

Nesta estratégia de recuperação de erro podemos estender a gramática da linguagem em mãos com produções que geram construções erradas. antecipando assim os erros mais comuns. O analisador sintático pode. gerar diagnósticos de erro apropriado sobre a construção errônea que foi reconhecida na entrada. então. .

Infelizmente. gostaríamos que um compilador fizesse o mínimo de mudanças possível no processamento de uma cadeia de entrada incorreta. de modo que essas técnicas têm atualmente interesse meramente teórico. esses métodos em geral são muito caros em termos de tempo e espaço de implementação.Idealmente. . Existem algoritmos que auxiliam na escolha de uma sequência mínima de mudanças afim de obter uma correção com um custo global menor.

Elaborar uma gramática para reconhecer: ◦ expressões relacionais ◦ declaração de variáveis ◦ chamada de funções Criar a árvore de derivação para as expressões: ◦ a-b*c ◦ (a+b)*c ◦ a+(b*c)/d .

<programa> ::= program <identificador> . program identificador . bloco . <bloco> . .

<bloco> ::= [<declar. variavel . variavel>] <comando composto> comando composto declar.

vars .<declar. vars>} var declar. Vars> {<declar. variavel> ::= var <declar.

vars> ::= <lista identificadores> : <tipo> . . lista identificadores : tipo .<declar.

<identificador>} identificador . .<lista identificadores> ::= <identificador> {.

<tipo> ::= integer | real | string integer real string .

<comando> } end begin comando end . .<comando composto> ::= begin <comando> {.

<comando> ::= <atribuicao> | <chamada procedimento> | <desvio condicional> | <laco repeticao> | <comando composto> | <vazio> atribuicao chamada procedimento desvio condicional laco repeticao comando composto .

<atribuicao> ::= <identificado> := <expressao> identificador := expressao .

<chamada procedimento> ::= <identificador> [(<expressao>{. ) .<expressao>})] identificador ( expressao .

<desvio condicional> ::= if <expressao> then <comando> [ else <comando> ] if expressao then comando else comando .

<laco repeticao> ::= while <expressao> do <comando> while expressao do comando .

simples operador relacional expres. simples . simples>] expres. simples> [<operador relacional> <expres.<expresao> ::= <expres.

<operador relacional> ::= > | >= | < | <= | <> | = > >= < <= <> = .

simples> ::= [+|-] <termo> {(+|-|OR) <termo>} + termo + termo or .<expres.

<termo> ::= <fator> {(*|DIV|AND) <fator>} fator * fator div and .

<fator> ::= <identificador> | <numero> | <chamada funcao> | (<expressao>) | NOT <fator> variavel numero chamada funcao ( expressao ) NOT fator .

<chamada funcao> ::= <identificador> [(<lista expressoes>)] identificador ( lista expressoes ) .

<expressao>} expressao . .<lista expressoes> ::= <expressao> {.

vars> ::= <lista identificadores> : <tipo> . simples> ::= [+|-] <termo> {(+|-|OR) <termo>} <termo> ::= <fator> {(*|DIV|AND) <fator>} <fator> ::= <identificador> | <numero> | <chamada funcao> | (<expressao>) | NOT <fator> <chamada funcao> ::= <identificador> [(<lista expressoes>)] <lista expressoes> ::= <expressao> {. <identificador>} <tipo> ::= integer | real | string <comando composto> ::= begin <comando> {.. <expressao>} <numero> ::= {digito}+[. simples> [<operador relacional><expres..<programa> ::= program <identificador> . <comando> } end <comando> ::= <atribuicao> | <chamada procedimento> | <desvio condicional> | <laco repeticao> | <comando composto> | <vazio> <atribuicao> ::= <identificador> := <expressao> <chamada procedimento> ::= <identificador> [(<expressao>{. simples>] <operador relacional> ::= > | >= | < | <= | <> | = <expres. variavel> ::= var <declar.|x|y|z|A|B|C|D|. <bloco> .{digito}+] <identificador> ::= <letra> {<letra> | <digito>} <letra> ::= a|b|c|d|e|f|.<expressao>})] <desvio condicional> ::= if <expressao> then <comando> [ else <comando> ] <laco repeticao> ::= while <expressao> do <comando> <expresao> ::= <expres. <bloco> ::= [<declar.|X|Y|Z <digito> ::= 0|1|2|3|4|5|6|7|8|9 . vars> {<declar.. <lista identificadores> ::= <identificador> {. vars>} <declar.. variavel>] <comando composto> <declar.

de/~bernhard/Pascal-EBNF... .CONST FUNCTION e PROCEDURE (declaração) TYPE RECORD ARRAY BOOLEAN POINTER FILE . Em http://www.lrz.html encontra-se a definição EBNF completa da gramática para linguagem Pascal.

Receber o lista de tokens ◦ Os tokens gerados pelo analisador léxico (armazenados numa fila) são passados ao analisador sintático. . Gerar a árvore de derivação ◦ Cada token que for validado deve ser armazenado numa árvore. Reconhecer a gramática ◦ A sequência de tokens deverá ser reconhecida e validada pela gramática.

void nextToken(){ if(p<strlen(input)) { token = input[p++].#include <iostream> #define TRUE 1 #define FALSE 0 // <EXPRESSAO> // <OPERADOR> // <TERMO> ::= <TERMO> | <EXPRESSAO> <OPERADOR> <TERMO> ::= + | ::= 0|1|2|3|4|5|6|7|8|9 char input[256]. } } . } else { token = '\0'. int p = 0. char token.

int isOperator(void) { if(token=='+' || token=='-') { nextToken(). } } int isTerm(void) { if(isdigit(token)) { nextToken(). } } . } else { return FALSE. } else { return FALSE. return TRUE. return TRUE.

} } else { return FALSE. } else if(isOperator()) { return isExpression(). } .int isExpression() { if(isTerm()) { if(token=='\0') { return TRUE. } } int parser() { nextToken(). } else { return FALSE. return isExpression().

gets(input). return 0.int main(){ printf("Digite uma expressão: "). getchar(). } . if(parser()) printf("Expressão válida"). else printf("Expressão inválida").

.1. também identificadores <EXPRESSAO> ::= <TERMO> | <EXPRESSAO> <OPERADOR> <TERMO> <OPERADOR> <TERMO> <NUMERO> <IDENTIF> ::= + | ::= <NUMERO> | <IDENTIF> ::= 0|1|2|3|4|5|6|7|8|9 ::= A|B|C|D|E|. Aceitar. como termo..|Z .

|Z ...2. Adicionar na gramática os fatores de multiplicação e divisão <EXPRESSAO> ::= <TERMO> { <OPER_ADD> <TERMO> } <TERMO> <OPER_ADD> ::= <FATOR> { <OPER_MULT>| <FATOR> } ::= + | - <OPER_MULT> ::= * | / <FATOR> <NUMERO> <IDENTIF> ::= <IDENTIF> | <NUMERO> | (<EXPRESSAO) ::= 0|1|2|3|4|5|6|7|8|9 ::= A|B|C|D|E|.

<identificador>} <tipo> ::= i | r | s <identificador> ::= A|B|C|D|E|. {<declar.. vars> ::= <lista identificadores> : <tipo> <lista identific. <declar.3. vars>.. vars>. validar se a declaração de variáveis está sintaticamente correta.> ::= <identificador> {.|Z . De acordo com a gramática abaixo. variavel> ::= v <declar.} <declar.

. |z . <bloco> <ident> ::= b e ::= a|b|c|d|e|f| . <bloco> . validar se a estrutura de um programa está correta.4. De acordo com a gramática abaixo. <program> ::= p <ident> ..

struct No *dir. }. struct No { info dados. struct No *esq. . typedef struct No no.#include <iostream> #define #define #define #define #define CMD_INSERIR '1' CMD_LISTAR '2' CMD_SAIR '0' TRUE 1 FALSE 0 typedef struct { int valor. } info. Isto é apenas um exemplo de árvore para ser adaptado para a árvore sintática. Um compilador não precisa utilizar árvore binária.

} no * criarNo(info d) { no *novo = (no *) malloc(sizeof(no)). Listar\n"). novo->esq = NULL. scanf("%d". novo->dir = NULL. printf("2. Inserir\n"). printf("1. } void lerDados(info *dados) { printf("Digite um valor: ").char menu() { system("cls"). } return novo. printf("\nInforme sua opção: "). if(novo!=NULL) { novo->dados = d. Sair\n"). &dados->valor). return getchar(). printf("0. } .

novo = criarNo(d).valor > (*raiz)->dados. d).int inserir(no **raiz. } } return TRUE.valor) inserir(&(*raiz)->dir.valor < (*raiz)->dados. if(novo==NULL) { return FALSE. } . if(d. } else { if(*raiz == NULL) { *raiz = novo. info d) { no *novo.valor) inserir(&(*raiz)->esq. } else { if(d. d).

liberar(&(*raiz)->dir).(*raiz)->dados.valor). } } . free(*raiz). printf("%d\n". listar(&(*raiz)->dir).void listar(no **raiz) { if(*raiz) { listar(&(*raiz)->esq). } } void liberar(no **raiz) { if(*raiz) { liberar(&(*raiz)->esq).

int main(void) { no *raiz = NULL; info dados; char opcao; do { opcao = menu(); switch(opcao) { case CMD_INSERIR: lerDados(&dados); inserir(&raiz, dados)==TRUE ? printf("Sucesso ao inserir!\n") : printf("Erro ao inserir!\n"); system("pause"); break; case CMD_LISTAR: listar(&raiz); system("pause"); break; } } while(opcao != CMD_SAIR); liberar(&raiz); return 0; }

Analisador Léxico ANÁLISE Analisador Sintático Analisador Semântico Gerador de código intermediário Otimizador de código Gerador de Código-objeto

Tratamento de Erros

SÍNTESE

Tabela de Símbolos

A Análise Semântica - terceira fase da compilação descreve o significado de construções sintáticas.

Fase Léxica Sintática Semântica

Entrada Cadeia de Caracteres Cadeia de Tokens Árvore Sintática

Saída Cadeia de Tokens Árvore Sintática Árvore com Anotações

Analisador Semântico Árvore Sintática Árvore com Anotações .Sua função é verificar se existem erros semânticos na árvore sintática e coletar as informações necessárias para a próxima fase da compilação. que é a geração de código objeto.

Análise Semântica é por vezes referenciada como análise sensível ao contexto porque lida com questões dependentes de contexto, o que está além das capacidades de uma GLC.
◦ Por exemplo: A := B + C; ◦ É ilegal em Pascal se:
houverem variáveis não declaradas houverem variáveis de tipos diferentes houverem variáveis declaradas como booleanas

Que tipo de significado está envolvido e que excede a capacidade das GLCs:
◦ X foi declarado apenas uma vez? ◦ X foi declarado antes do seu primeiro uso? ◦ X foi definido antes do seu primeiro uso? ◦ X é um escalar, um array, uma função, ou uma classe? ◦ X é declarado mas nunca utilizado? ◦ Os tipos de uma expressão são compatíveis? ◦ As dimensões casam com o declarado?

Semântica Estática
◦ Em tempo de compilação

Dinâmica
◦ Em tempo de execução

Conjunto de restrições que determinam se programas sintaticamente corretos são válidos. Compreende checagem de tipos, análise de escopo de declarações, checagem de número e tipo de parâmetros de funções/procedimentos. É realizado em tempo de compilação em linguagens tipadas, que exigem declarações, tais como C, Pascal, Java, etc. Pode ser especificada informalmente (e geralmente é artesanal) através de descrições em manuais de cada linguagem ou formalmente, por exemplo, com uma Gramática de Atributos.

etc. PROLOG. PHP.Ocorre em tempo de execução em linguagens em que as variáveis são determinadas pelo contexto de uso. tais como LISP. É geralmente especificada informalmente nos manuais mas também existem modelos formais: ◦ Vienna Definition Language (VDL) ◦ Definições Axiomáticas ◦ Modelos Denotacionais ◦ Modelos Operacionais ◦ Gramática de Atributos .

As tarefas básicas desempenhadas durante a análise semântica incluem verificação de: ◦ Declarações ◦ Tipos todas as variáveis foram declaradas? O tipo declarado é o correto para o operador? O comando é válido nesse contexto? O número e o tipo dos parâmetros são compatíveis? O identificador é único no escopo? ◦ Fluxo de controle ◦ Parâmetros ◦ Unicidade .

a = b + 5. No exemplo.Todas as variáveis devem ser declaradas. o identificador b não está declarado. . int a.

. tipos indicando que o operador módulo % não pode ter um operador real.Considere o seguinte exemplo de código: int f1(int a. } O analisador semântico detecta um erro na verificação de tipos. float b) { return a%b.

.5. int vetor[3]. vetor[3] = 20. outro erro semântico ocorre ao tentar acessar um índice fora da faixa de valores válidos. float a = 2.O compilador deve relatar um erro se um número real for usado para indexar um array. Além do tipo. vetor[a] = 10.

. b. na expressão: int a.Em alguns casos. o compilador realiza a conversão automática de um tipo para outro que seja adequado à aplicação do operador. Por exemplo. a = b – '9' A constante do tipo caráter '9' é automaticamente convertida para inteiro para compor corretamente a expressão aritmética na qual ela toma parte.

.Outro exemplo de erro detectado pela análise semântica. } } Ele reconhece que o comando break só pode ser usado para quebrar a sequência de um comando de iteração (loop) ou para indicar o fim de um case (switch). controle é ilustrado pelo código: void f2(int j. int k) { if (j == k) { break. neste caso pela verificação de fluxo de controle.

int b. a compilação do seguinte código: int sum(int a.. Dois erros semânticos ocorrem: ◦ A chamada a função está passando apenas 2 parâmetros ◦ O segundo parâmetro é incompatível com o protótipo .. x = sum(y.A verificação de parâmetros em chamadas à funções e procedimentos deve checar o número e o tipo de parâmetros. int c) { return (a+b+c). Por exemplo.1). } . 5.

dois campos com nome b . } a. b: float. struct { b: int. em declarações de variáveis.A verificação de unicidade detecta situações tais como duplicação. Dois erros semânticos ocorrem: ◦ Há duas declarações de variável com nome a ◦ Há. a compilação do seguinte código: int a. dentro do escopo. Por exemplo. no registro. de componentes de estruturas e em rótulos do programa.

Os nomes servem para representar várias entidades dentro de um programa. O escopo pode ser local ou global. Podemos dizer que o escopo de uma variável representa a região na qual o acesso ao seu conteúdo é visível. função. classe. etc. . seja uma variável. É mais simples se referir a uma variável pelo seu nome do que pela sua localização na memória.

x = 5. return s+x. } return s. y = g(). i<=n. } int g() { int s.int x. } x somatorio s g s main a n y for i . i++) { s += i. x++. int somatorio(int n) { int s = 0. for (int i=1. } void main() { int a. s = somatorio(10). y.

deve se encontrar a declaração dessa variável no escopo atual ou nos seus escopos ancestrais. escondem as variáveis definidas no pai estático. . para resolver conflito de nomes.A maioria das linguagens de programação implementa escopo estático e. aplica a regra do contexto envolvente mais próximo (escopo ancestral mais próximo). Variáveis com o mesmo nome. mas em escopos diferentes. Para vincular uma referência a uma variável.

var x. end.Como diferenciar variáveis globais de locais na TS program meu_prog. y: integer. begin read(y). Variável y local x:=x+y. x:=x*y. end. Variável y global . begin read(y). procedure meu_proc(x: integer) var y: real.

Inclusão de um campo a mais na tabela de símbolos indicando o nível da variável no programa ◦ Controle do nível durante a compilação do programa Quando se chama um procedimento (ou função).1. Tabelas diferentes para diferentes escopos . Associação das variáveis locais a um procedimento (ou função) à entrada relativa ao procedimento (ou função) por meio. por exemplo. faz-se nível:=nível+1 Quando se sai de um procedimento (ou função). ◦ Atenção: para a checagem de tipos. de uma lista encadeada. deve-se saber quantos são e quais são os parâmetros de um procedimento (ou função) na tabela de símbolos 3. faz-se nível:=nível-1 Busca do fim para o início da TS a fim de encontrar a declaração mais recente 2.

usa‐se a tabela de símbolos para realizar/computar a semântica. pode ser utilizada a tradução dirigida pela sintaxe com gramáticas de tributos.Geralmente. . Mais formalmente.

Um compilador usa uma Tabela de Símbolos (TS) para guardar informações sobre os nomes declarados em um programa. A TS serve para saber:
◦ Quais símbolos foram definidos? ◦ O que o símbolo representa? ◦ Qual escopo de validade do símbolo?

Uma TS é uma estrutura de dados que guarda as informações necessárias para cada nome visto no programa, seja um nome de variável, de tipo, de classe ou de qualquer outro conceito na linguagem fonte. Esta tabela é usada durante várias etapas da compilação e, algumas vezes, também na depuração de programas. Após a árvore de derivação, a tabela de símbolos é o principal atributo herdado em um compilador.

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. O gerenciamento 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 pode ser implementada como um registro ("record" ou "struct") contendo campos (nome, tipo, classe, tamanho, escopo, etc.) que a qualificam.

Como é necessário obter as informações relativas a um símbolo (nome) cada vez que ele ocorre no texto do programa-fonte, a tabela de símbolos deve ser indexada pelo próprio nome do símbolo, ou seja, dado o nome da variável, tipo ou função, é necessário obter as informações para aquele nome. De preferência, a busca pelas informações de um nome deve ser rápida e eficiente, tanto para obter as informações associadas quanto para atualiza-las.

Se for utilizada uma estrutura como um array ou uma lista, a busca pelo nome do símbolo terá complexidade linear O(n) no tempo, onde n é o tamanho da tabela. Uma outra alternativa seria utilizar alguma estrutura de árvore balanceada, como as árvores AVL. Nesse caso a complexidade no tempo cai para logarítmica O(log n), o que já pode significar um ganho de desempenho considerável. Mas é possível obter uma estrutura ainda mais eficiente para busca direta de símbolos: a chamada tabela de dispersão ou tabela hash. Nesta, a busca por um nome tem complexidade praticamente constante (O(1)), independendo do número de símbolos utilizados pelo programa.

Conceitualmente, estas tabelas são compostas de pares (chave, valor) onde a chave é alguma informação utilizada para indexar o valor. Uma tabela hash para um dado tipo de chave consiste de:
◦ Uma função hash h ◦ Um arranjo (tabela) de tamanho N

Uma função hash h mapeia chaves de um dado tipo em inteiros em um intervalo fixo [0, N - 1] O valor inteiro h(k) Є [0, N - 1] é chamado de valor hash da chave k

Em suma, os dois componentes principais de uma tabela hash são:
1. A função de hash 2. A técnica de resolução de colisão

E as operações que uma tabela hash oferece são duas:
1. Buscar de um item, através da chave; e, 2. Inserir um novo item.
Opcionalmente, pode-se ter uma operação para remover itens.

961. num cadastro de pessoas. guardamos seus dados na posição 66. Mas. podemos criar um arranjo de tamanho N = 100 e a seguinte função hash: h(k) = 2 últimos dígitos de k. se tivermos um conjunto restrito de pessoas.Por exemplo. . se o CPF da pessoa for 347. e o valor seria o conjunto de informações guardado sobre a pessoa. Assim. é possível compactar o valor do índice. poderíamos usar o CPF como chave. sendo portanto possíveis 99 bilhões de valores para a chave. Para otimizar o uso de recursos. O número de CPF tem 11 dígitos.571-66.

249.039-09 30 162.055-00 9 676.864-99 .532.534-30 99 564.0 257.752.226.

para encontrar um elemento.Colisões ocorrem quando diferentes chaves são mapeadas sem distinção na mesma célula da tabela. em uma tabela hash bem dimensionada. . em média. Note que é impossível evitar completamente colisões se o fator de carga (load factor) l de uma tabela hash for l>1 Esse fator é definido como a razão entre o no. n de chaves pelo tamanho da tabela N: l = n/N Uma possibilidade para resolver o problema das colisões é ter uma lista de entradas associada a uma posição na tabela. devemos ter 1. Considera-se que.5 acessos à tabela.

752.0 257.447.142.864-99 .249.039-09 731.500-09 178.534-30 383.689-30 99 564.055-00 9 676.532.226.196.933-09 30 162.

caracteres Análise Léxica tokens Análise Sintática árvore sintática Análise Semântica árvore com anotações .

programa #$%&*. c := 2. f := a + b. end.101: boolean. var a.e: string. end. f.5. d := 'yes. begin a := 987654321.c: integer c.b. if a > 3 do a := 5.x. end . while b+1 < a bgin b := b + d.d. b[1] := !1. g := 2a / 0.

while a <= b do begin a := a + (a*3/100). c: integer. b := 7000. var a.b: real. if c < 35 then write('Meta atingida') else write('Meta não atingida'). .program meta. c := 0. end. begin a := 5000. c := c + 1. b := b + (b*2/100). end.

otimização e geração .código intermediário.

2. dividida em 3 partes: 1.Após realizada a análise. de forma a gerar um código de máquina mais eficiente. 3. na fase de síntese o compilador deverá gerar o código em linguagem simbólica equivalente ao código analisado. Neste ponto do projeto. Tipicamente. finda-se a parte de análise e entramos no processo de síntese. O gerador de código objeto tem como objetivo analisar o código já otimizado e gerar o código objeto definitivo para uma máquina alvo. . O otimizador de código tenta melhorar o código intermediário. O gerador de código intermediário permite a geração de instruções para uma máquina abstrata.

.

Analisador Léxico ANÁLISE Analisador Sintático Analisador Semântico Gerador de código intermediário Otimizador de código Gerador de Código-objeto Tratamento de Erros SÍNTESE Tabela de Símbolos .

O gerador de código intermediário será acionado quando o programa for analisado léxica. sintática e semanticamente. . o gerador de código intermediário usa as estruturas produzidas pelas fases de análise para criar uma sequência de instruções para uma máquina abstrata dita como código intermediário. Ou seja. e estiver correto do ponto de vista das três análises citadas.

Para essa geração. na maioria das vezes. Esse código pode. A desvantagem é que a tradução direta leva a uma compilação mais rápida. para que o código seja gerado das folhas para os nós.A geração de código intermediário é. portanto. a transformação da árvore de derivação em um segmento de código. deve-se percorrer a árvore em profundidade (depth first). constitui-se num código intermediário. mas. . eventualmente. ser o código final.

A grande diferença entre o código intermediário e o código objeto final é que o intermediário não especifica detalhes da máquina alvo. tais como quais registradores serão usados. etc. Um exemplo clássico desse modelo é a linguagem Java. que gera seu bytecode compatível com diferentes arquiteturas. . quais endereços de memória serão referenciados.

. resolvendo. de modo a obter-se o código objeto final mais eficiente.Possibilita a otimização intermediária. gradativamente. Simplifica a implementação do compilador. Possibilita a tradução do código intermediário para diversas máquinas. as dificuldades da passagem de código fonte para objeto.

. ◦ Notação pós-fixada ou pré-fixada ◦ Código de três-endereços: quádruplas e triplas.Os vários tipos de código intermediário fazem parte de uma das seguintes categorias: ◦ Representações gráficas: árvore e grafo de sintaxe.

Os operadores constituem nós interiores da árvore. . além de incluir as simplificações da árvore de sintaxe. faz a fatoração das subexpressões comuns. Um grafo de sintaxe. eliminando-as.Uma árvore de sintaxe é uma forma condensada de árvore de derivação na qual somente os operandos da linguagem aparecem como folhas.

:= a * b c b + * c a := + * b c Árvore de Sintaxe Grafo de Sintaxe .

então “E1 E2 q” é a representação pós-fixada para a expressão “E1 q E2”. então “q E1 E2” é a representação préfixada para a expressão “E1 q E2”. E1 e E2 são expressões préfixadas. Notação Infixada (a+b)*c a*(b+c) a+b*c Pós-fixada ab+c* abc+* abc*+ Pré-fixada *+abc *a+bc +a*bc . por outro lado. Se.Se E1 e E2 são expressões pós-fixadas e q é um operador binário.

.

cada instrução faz referência. Um código de três-endereços pode ser implementado através de quádruplas ou triplas.No código intermediário de três-endereços. a três variáveis. Código de três-endereços para o comando A := X + Y * Z T1 := Y * Z T2 := X + T1 A := T2 onde T1 e T2 são variáveis temporárias. . no máximo.

As quadruplas são constituídas dos campos: um operador. Exemplo: comando A := B*(-C+D) em quádruplas Oper (0) (1) (2) (3) -u + * := Arg1 C T1 B T3 D T2 Arg2 Result T1 T2 T3 A . dois operandos e o resultado.

Essa representação utiliza ponteiros para a própria estrutura. evitando a nomeação explícita de temporários. Exemplo: comando A := B*(-C+D) em triplas Oper (0) (1) (2) (3) -u + * := Arg1 C (0) B A D (1) (2) Arg2 .As triplas são formadas por: um operador e dois operandos.

a expressão em C a = b + c * d.Por exemplo. Seria traduzida para as instruções: _t1 := c * d a := b + _t1 .

Instruções de desvio condicional tem 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. . a linha seguinte é executada. caso contrário.

x[0] = 0. a seguinte iteração em C while (i++ <= k) x[i] = 0.Por exemplo. Poderia ser traduzida para _L1: if i > k goto _L2 i := i + 1 x[i] := 0 goto _L1 _L2: x[0] := 0 .

.

Analisador Léxico ANÁLISE Analisador Sintático Analisador Semântico Gerador de código intermediário Otimizador de código Gerador de Código-objeto Tratamento de Erros SÍNTESE Tabela de Símbolos .

O otimizador de código (independente de máquina) é um módulo opcional (presente na grande maioria dos compiladores) que objetiva melhorar o código intermediário de modo que o programa objeto produzido ao fim da compilação seja menor (ocupe menos espaço de memória) e/ou mais rápido (tenha tempo de execução menor). A saída do otimizador de código é um novo código intermediário. .

Aspectos de uso de memória e de velocidade de execução são muitas vezes conflitantes. Na prática. ganhos de espaço utilizado implicam perdas no tempo de execução. o que se faz é utilizar heurísticas (técnicas ad hoc ou empíricas) para tentar otimizar o código ao máximo. Sabe-se que a geração de código ótimo é um problema indecidível. em geral. pois. . e vice-versa.

A otimização de código objeto é realizada através da troca de instruções de máquina por instruções mais rápidas e de melhor utilização dos registradores. suprimir subexpressões comuns. etc. o processo de otimização desenvolve-se em duas fases: ◦ Otimização do código intermediário ◦ Otimização do código objeto A primeira inclui técnicas para eliminar atribuições redundantes.Normalmente. de modo a obter um código intermediário menor.. eliminar temporários desnecessários. . trocar instruções de lugar.

como: C E E T → → → → V=E E+T E-T T*F . Add. Quando analisamos código para este comando. com a semântica usual. Sub e Mult. Store.Em uma máquina com um registrador principal (acumulador) e instruções Load. identificamos o uso de várias regras.

Geramos o seguinte código intermediário e as seguintes instruções em código objeto: t1 = a + b (regra E → E+T) t2 = c + d regra E → E-T t3 = t1 * t2 (regra T → T*F) x = t3 (regra C → V=E) Load a Add b Store t1 Load c Sub d Store t2 Load t1 Mult t2 Store t3 Load t3 Store x .

11 instruções 1 2 3 4 5 6 7 8 9 10 11 Load a Add b Store t1 Load c Sub d Store t2 Load t1 Mult t2 Store t3 Load t3 Store x 9 instruções 1 2 3 4 5 6 7 8 9 Load a Add b Store t1 Load c Sub d Store t2 Load t1 Mult t2 Store x 7 instruções 1 2 3 4 5 6 7 Load a Add b Store t1 Load c Sub d Mult t1 Store x .

e visa apenas mostrar as possibilidades existentes. .Eliminação de subexpressões comuns Eliminação de código morto Renomeação de variáveis temporárias Transformações algébricas Dobramento de constantes Otimização de loop Eliminação de variáveis de indução Esta lista não é exaustiva.

. dispensando o uso da temporária t1.. ◦ Se os valores de a e de b não são alterados. é possível guardar o valor da expressão a+b em uma temporária (digamos t1) e usá-lo outra vez posteriormente. x = a + b. se a variável x ainda está disponível com o mesmo valor da segunda vez que a expressão é calculada. . .. y = x.x = a + b. .. t1 = a + b x = t1... ◦ ou.. y = a + b. y = t1.

a. y = a + b. devemos considerar.. Neste caso. que todas as variáveis do tipo apontado foram alteradas.. .. b.. . e portanto não podemos garantir que *p não é um pseudônimo para a ou para b. Considere o exemplo: int *p. *p = . por segurança.. não sabemos para qual inteiro p aponta.No caso de variáveis ponteiros. ... x = a + b.

. isso pode acontecer em várias situações. não se espera que um programador escreva código morto. uma das quais é um estágio intermediário da otimização de um programa. que sofreu algumas transformações que causaram a existência de código não alcançável. Entretanto. O código morto (dead code) pode ser identificado em uma série de situações.Normalmente.

/* este código não será executado */ . ... goto x. e não é alvo de nenhum outro desvio. if(DEBUG) { .... /* o incremento não será executado */ } Ocorre após um teste com uma condição impossível de ser satisfeita.Ocorre após uma instrução de encerramento de um programa ou de uma função.. i=3. /* este código não será executado */ } Ocorre após um comando de desvio. int f(int x) { return x++. x: ... #define DEBUG 0 .

Por exemplo.Já vimos em várias ocasiões que as variáveis temporárias introduzidas durante a geração de código intermediário podem não ser estritamente necessárias.. . o código intermediário será t1 = a + b. x = t1. e a variável t1 pode ser eliminada. se tivermos no código fonte x=a+b.

identidade. como comutatividade. podemos transformar x=a+b*c. como a soma é comutativa.Podemos aplicar algumas transformações baseadas em propriedades algébricas. Por exemplo. associatividade. em x=b*c+a. o que corresponde a trocar código como: Load b Mult c Store t1 Load a Add t1 Store x Load b Mult c Add a Store x . etc.

Transformações semelhantes podem ser baseadas na associatividade. o que corresponde a trocar: Load a Add b Store t1 Load c Add d Store t2 Load t1 Add t2 Store x Load a Add b Add c Add d Store x . permitindo trocar x=(a+b)+(c+d). por x=(((a+b)+c)+d).

Expressões ou sub-expressões compostas de valores constantes podem ser avaliadas em tempo de compilação (dobradas). Por exemplo.. Este valor pode ser pré-calculado. e substituído por 99.. while (i<N-1) { .. } não há necessidade de calcular repetidamente o valor de N-1. evitando sua avaliação repetida em tempo de execução. se tivermos: #define N 100 . ..

Há várias otimizações que se aplicam a loops. for (i=0. f(k*i). i<N. i++) { k=2*N. i<N. . i++) f(k*i). for (i=0. } que se transformaria em: k=2*N. a mais comum das quais é a transferência de computações invariantes do loop para fora dele.

f(15). f(45). f(25). i++) f(5*i). Pelo código objeto correspondente a: f( 5). f(40).Por exemplo. f(20). f(10). f(30). f(50). porque dispensa as dez multiplicações. dez operações de incremento e os onze testes de permanência do for. Teremos um código mais longo. i<=10. mas certamente mais rápido. f(35). . se substituirmos o código objeto correspondente a: for(i=1.

for (i=1. i<N.Outra possibilidade de otimização é a eliminação de variáveis de indução. .. a cada vez que o código do loop é executado. } j=0. z: if (j>2*N) goto x.. p(j). goto z. x: . p(j). ◦ Variáveis de indução são variáveis que assumem valores em progressão aritmética. i++) { j=2*i. j=j+2.

.

Analisador Léxico ANÁLISE Analisador Sintático Analisador Semântico Gerador de código intermediário Otimizador de código Gerador de Código-objeto Tratamento de Erros SÍNTESE Tabela de Símbolos .

. selecionando a forma de acessálos. Projetar um gerador de código que produza programas objeto verdadeiramente eficientes é uma das tarefas mais difíceis no projeto de um compilador. etc. definindo que registradores da UCP serão usados. tomando decisões com relação à alocação de espaço para os dados do programa.O gerador de código produz o código objeto final.

. quatro aspectos devem ser considerados no projeto de geradores de código: ◦ Forma do código objeto a ser gerado: Linguagem absoluta. ◦ Alocação de registradores. Algumas computações requerem menos registradores para resultados intermediários. relocável ou assembly.Basicamente. ◦ Seleção das instruções de máquina: A escolha da sequência apropriada pode resultar num código mais curto e mais rápido. ◦ Escolha da ordem de avaliação: A determinação da melhor ordem para execução das instruções é um problema insolúvel.

onde é altamente conveniente diminuir o custo de compilação. Compiladores deste tipo são utilizados em ambientes universitários.A geração de um programa em linguagem absoluta de máquina tem a vantagem de que o programa objeto pode ser armazenado numa área de memória fixa e ser imediatamente executado. Os compiladores que geram código absoluto e executam-no imediatamente são conhecidos como load and go compilers. .

Essa estratégia dá flexibilidade para compilar subrotinas separadamente e para chamar outros programas previamente Compilados. Módulos e objetos relocáveis podem ser ligados e carregados por um Ligador-Carregador. .A geração de código em linguagem de máquina relocável permite a compilação separada de subprogramas.

São geradas instruções simbólicas e podem ser usadas as facilidades de macro instruções.A tradução para linguagem assembly facilita o processo de geração de código. nas quais o compilador deve desenvolver-se em vários passos. É uma estratégia razoável. especialmente. O preço pago é um passo adicional: ◦ Tradução para linguagem de máquina. . para máquinas com pouca memória.

e muito problemática quando a máquina trabalha com registradores aos pares (para instruções de divisão e multiplicação).Instruções com registradores são mais curtas e mais rápidas do que instruções envolvendo memória. . A atribuição ótima de registradores a variáveis é muito difícil. Portanto. ou provê registradores específicos para endereçamentos e para dados. o uso eficiente de registradores é muito importante.

destino Instruções: ADD SUB MUL DIV LOAD COPY R1. R2 R1. R M R2 = R2 + R1 (adição) R2 = R2 – R1 (subtração) R2 = R2 * R1 (multiplicação) R2 = R2 / R1 (divisão) Registrador = Memória (carregamento) Memória = Registrador (armazenamento) R2 = R1 (cópia) STORE R. R2 R1. R2 R1.Familiaridade com a máquina e com o conjunto de instruções é pré-requisito para projetar um bom gerador de código. R2 M. Formato das instruções: op fonte. R2 . R1.

R0 c. R0 R0.a := b + c LOAD LOAD ADD STORE b. R1 R0. R1 b. a R0 = d R1 = c R1 = R1 * R0 R0 = b R0 = R0 + R1 a = R0 . R0 R0. R0 c. R1 R1. R0 R1. a R0 = b R1 = c R0 = R0 + R1 a = R0 a := b + c * d LOAD LOAD MUL LOAD ADD STORE d.

árvore com anotações Geração de Código Intermediário código intermediário Otimização de Código código otimizado Geração de Código Objeto código final .

4. 5. Traduzir para uma árvore de sintaxe Traduzir para um grafo de sintaxe Derivar código intermediário pós-fixado Derivar o código intermediário de três-endereços Otimizar o código intermediário gerado Gerar o código de máquina para a expressão .Dado o comando de atribuição x := (a+b) * c – (a+b) / d 1. 3. 6. 2.

◦ Linguagem de alto nível c = a + b. ax ◦ Linguagem de baixo nível . mov ax.Um compilador é um tipo de tradutor que lê um programa escrito numa linguagem. $a add ax. e transforma-o em um outro programa equivalente escrito em outra linguagem. a linguagem objeto. a linguagem fonte. $b mov $c.

. Além da tradução é importante destacar a tarefa do compilador na detecção de erros feitos pelo programador.É importante notar que o programa fonte e o programa objeto são completamente diferentes. porém possuem o mesmo significado.

A Ling. Ling. Z . X Maq. compilador Ling. C .A divisão de um compilador em front-end (análise) e back-end (síntese) reflete numa codificação modular com independência entre as fases. permite o reuso de código. em princípio. sendo possível escrever um front-end para cada linguagem de programação e um back-end para cada máquina alvo..... Maq. B Análise A Análise B AD e TS Síntese X Síntese Y Maq. Síntese Z ... Y .. Análise C . ◦ O que..

2002. Ana Maria de Alencar. João José. Gersting. 2004. Ed. Sagra Luzzato.. Prentice-Hall. 9 Elements of the Theory of Computation.Livro Texto: 1. Bookman. 1 exemplar. Neto. Judith L. Ítalo Santiago. Harrison. 2008. 5 exemplares. 1ª edição.. 5 exemplares. 2007. exemplar.. Técnicas. Modelagem e Implementação. 1 Introduction to Formal Language Theory. Fundamentos Matemáticos para a Ciência da Computação. Ramos.. Alfred et al. Louden. . Marcus V. Bookman. Lewis.. Linguagens Formais: Teoria. Implementação de Linguagens de Programação: Compiladores.. 5 exemplares. 10 exemplares. 17 exemplares. Linguagens Formais e Autômatos. Aho. B. 8. Menezes. Ed. Addison-Wesley. Thomson Pioneira. 2. 3. e Ferramentas. Paulo F. 5ª edição. Ed. Harry R. 7. Ed.. Michael A. Compiladores: Princípios e Práticas. Kenneth C. & Price. & Veja. 3ª edição. Ed. LTC. 5. Pearson. Bibliografia Complementar: 4. 2004. 2ª edição. M. Simão S. 1ª edição. Ed. 1978. Toscani. 2ª edição. Ed. 6. exemplares. 2009. Ed. 1998. 4ª edição. 1ª edição. Compiladores: Princípios.