Compiladores - IntroduçãoCompiladores - Introdução
Guilherme Amaral [email protected]
O que é um Compilador?O que é um Compilador?“Um compilador é um programa que lê um programa escrito em uma linguagem (linguagem fonte) e a traduz em um programa equivalente em outra linguagem (linguagem alvo).” Aho, Sethi, Ullman.
O que é um Compilador?O que é um Compilador?Nesse processo de tradução, há
duas tarefas básicas a serem executadas por um compilador:◦Análise, em que o texto de entrada
(na linguagem fonte) é examinado, verificado e compreendido Análise léxica, sintática e semântica
◦Síntese, ou geração de código, em que o texto de saída (na linguagem objeto) é gerado, de forma a corresponder ao texto de entrada
É possível representar completamente a sintaxe de uma LP através de uma gramática sensível ao contexto.
Mas como não existem algoritmos práticos para tratar essas gramáticas, a preferência recai em usar gramáticas livres de contexto.
Deixa-se para a análise semântica a verificação de todos os aspectos da linguagens que não se consegue exprimir de forma simples usando gramáticas livres de contexto.
A implementação de reconhecedores de linguagens regulares (autômatos finitos) é mais simples e mais eficiente do que a implementação de reconhecedores de linguagens livres de contexto (autômatos de pilha).
Nesse caso, é possível usar expressões regulares para descrever a estrutura de componentes básicos das LP, tais como identificadores, palavras reservadas, literais numéricos, operadores e delimitadores, etc.
Essa parte da tarefa de análise (análise léxica) é implementada separadamente, pela simulação de autômatos finitos.
Compiladores – Separando Compiladores – Separando em partesem partesUm dos modelos possíveis para a construção
de compiladores faz a separação total entre o frontend, encarregado da fase de análise, e o back-end, encarregado da geração de código, de forma que◦ O front-end e back-end se comunicam
apenas através da representação intermediária
◦ O front-end depende exclusivamente da linguagem fonte;
◦ O back-end depende exclusivamente da linguagem destino.
Simplifica a implementação de novos compiladores◦ Front-end específico para cada linguagem◦ Back-end específico para a arquitetura alvo
Um dos modelos possíveis para a construção de compiladores faz a separação total entre o front-end, encarregado da fase de análise, e o back-end, encarregado da geração de código, de forma que◦O front-end e back-end se comunicam
apenas através da representação intermediária
◦O front-end depende exclusivamente da linguagem fonte;
◦O back-end depende exclusivamente da linguagem objeto.
Verificador de TiposVerificador de Tipos
ParserParser
DesugarerDesugarer
Sintaxe Core
CoreToSTGCoreToSTG
Sintaxe STG
Gerador de ILGerador de IL
Código MSIL (Texto)
ILDASMILDASM
Gerador GHCGerador GHC
Código Assembly / Código C
Assembler / Compilador C
Assembler / Compilador C
Código Nativo
CorePrinterCorePrinter
Arquivo Core
PhxSTGCompilerPhxSTGCompiler
JITJIT
Código Nativo
Arquivo Haskell '
Otimizações
Assembly MSIL
Front-end
Back-end
Compiladores - FasesCompiladores - FasesConjunto de alterações feitas no
código as quais são responsáveis por uma atividade específica do processo de compilação◦Análise Léxica (scanner)◦Análise Sintática (parser)◦Análise Semântica◦Otimização◦Geração de código
Analisador léxico
Analisador sintático
Analisador semântico
Gerador de código
intermediário
Otimizador de código
Gerador de código
código fonte
código alvo
Tratador de erros
Gerador tabela de símbolos
Análise LéxicaAnálise LéxicaTambém chamada de scanner Agrupa caracteres em símbolos (ou
tokens) Entrada: fluxo de caracteres Saída: fluxo de símbolos Símbolos são:
◦Palavras reservadas, identificadores de variáveis e procedimentos, operadores, pontuação,...
Uso de expressões regulares no reconhecimento
Lex/Flex são ferramentas que geram scanners.
Análise LéxicaAnálise LéxicaDado os caracteres da instrução
montante := saldo + taxa_de_juros * 30;
São identificados os seguintes tokens:Identificador montantmontantSímbolo de atribuição :=:=Identificador saldosaldoSímbolo de adição ++Identificador taxa_de_jurostaxa_de_jurosSímbolo de multiplicação **Número 3030
Análise SintáticaAnálise SintáticaTambém chamada de parserAgrupa símbolos em unidades sintáticasEx.: os 3 símbolos A+B podem ser
agrupados em uma estrutura chamada de expressão.
Expressões depois podem ser agrupados para formar comandos ou outras unidades.
Saída: representação árvore de parse do programa
Gramática livre de contexto é usada para definir a estrutura do programa reconhecida por um parser
Yacc/Bison são ferramentas para gerar parsers
Análise SintáticaAnálise SintáticaComand
o
Expressão
Identificador
Expressão
Expressão
Expressão
Expressão
:=
+
*
Identificador
Identificador
montante
saldo
taxa_de_juros
Número
60
Árvore gerada para: montante := saldo + taxa_de_juros * 60
Análise SemânticaAnálise SemânticaVerifica se estruturas sintáticas, embora
corretas sintaticamente, têm significado admissível na linguagem.
Por exemplo, não é possível representar em uma gramática livre de contexto uma regra como “todo identificador deve ser declarado antes de ser usado“
Um importante componente é checagem de tipos.
Utiliza informações coletadas anteriormente e armazenadas na tabela de símbolos
Considerando “A + B”, quais os possíveis problemas semânticos?
Saída: árvore de parse anotada
Análise SemânticaAnálise Semântica
montante
+
*
saldo
taxa_de_juros
*
60
montante
+
*
saldo
taxa_de_juros
*
inttorealinttoreal
Conversão de inteiro para real inserida pela análise semântica
Gerador de Código Gerador de Código IntermediárioIntermediárioUsa as estruturas produzidas
pelo analisador sintático e verificadas pelo analisador semântico para criar uma seqüência de instruções simples (código intermediário)
Está entre a linguagem de alto nível e a linguagem de baixo nível
Gerador de Código Gerador de Código IntermediárioIntermediárioConsidere que temos um único
registrador acumulador. Considere o comando de atribuição
x := a + b * c pode ser traduzido em:
t1:=b*ct2:=a+t1x:=t2Pode-se fazer um gerador de código
relativamente simples usando regras como:
Gerador de Código Gerador de Código IntermediárioIntermediário Toda operação aritmética (binária) gera 3
instruções. Para b*c1. Carga do primeiro operando no acumulador
load b2. Executa a operação correspondente com o segundo
operando, deixando o resultado no acumuladormult c
3. Armazena o resultado em uma nova variável temporáriastore t1
Um comando de atribuição gera sempre duas instruções. Para x:= t2
1. Carrega o valor da expressão no acumuladorload t2
◦ Armazena o resultado na variávelstore x
Para o comando de atribuiçãox := a + b * c;
é gerado o código intermediário:1. Load b { t1 := b * c }2. Mult c3. Store t14. Load a { t2 := a + t1 }5. Add t16. Store t27. Load t28. Store x { x := t2 }
Otimizador de CódigoOtimizador de Código Independente da máquina Melhora o código intermediário de modo que o programa objeto 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
Otimizador de CódigoOtimizador de Código1. Load b2. Mult c3. Store t14. Load a5. Add t16. Store t27. Load t28. Store x
1. Load b2. Mult c3. Add a4. Store x
Otimizador de CódigoOtimizador de Código
Gerador de CódigoGerador de CódigoProduz o código objeto final Toma decisões com relação à:
◦ Alocação de espaço para os dados do programa;
◦Seleção da forma de acessá-los;◦Definição de quais registradores serão
usados, etc.Projetar um gerador de código que
produza programas objeto eficientes é uma das tarefas mais difíceis no projeto de um compilador
Gerador de CódigoGerador de CódigoVárias considerações têm que ser feitas:
◦Há vários tipos de instruções correspondendo a vários tipos de dados e a vários modos de endereçamento;
◦Há instruções de soma específicas, por exemplo para incrementar/decrementar de 1;
◦ Algumas somas não foram especificadas explicitamente:
◦Cálculo de endereço de posições em vetores;◦ Incremento/decremento registrador de topo
pilha◦Local onde armazenar variáveis;◦Alocação de registradores