Projeto de Linguagem de Programação
Aula 2: Fundamentos do processo de compilação
Prof. Joaquim Pessôa Filho
Sumário
1. Níveis de Linguagens de Programação
2. Qualidades da Linguagem
3. Tipos de Processadores de Linguagem
4. Diagramas Tombstone
1. Compiladores x Interpretadores
2. Máquinas Reais e Abstratas
3. Técnicas de Bootstrapping
Níveis de Linguagens de Programação
O que é um programa escrito em código de máquina ou linguagem de máquina?
É uma seqüência de instruções, sendo que, cada
instrução é uma cadeia de bits que é interpretada pela máquina para executar uma determinada operação.
É fácil ler, escrever e editar um programa escrito em linguagem de máquina?
Não, dessa forma, foram criadas notações simbólicas para facilitar a leitura, escrita e edição de programas.
Níveis de Linguagens de Programação
Exemplo:
√(s × (s - a) × (s - b) × (s - c) ) onde s = ( a + b + c )/ 2
Níveis de Linguagens de Programação
Exemplo codificado em linguagem Assembly:
LOAD R1 a; ADD R1 b; ADD R1 c; DIV R1 #2;
LOAD R2 R1;
LOAD R3 R1; SUB R3 a; MULT R2 R3;
LOAD R3 R1; SUB R3 b; MULT R2 R3;
LOAD R3 R1; SUB R3 c; MULT R2 R3;
LOAD R0 R2; CALL sqrt
Níveis de Linguagens de Programação
Exemplo codificado em uma linguagem que apresenta uma notação similar à notação matemática já
conhecida:
let s = (a + b + c)/2
in sqrt (s * (s - a) * (s - b) * (s - c))
Qualidades da Linguagem
A principal virtude de uma linguagem é a sua legibilidade, isto é, a facilidade que a linguagem
oferece para que um programador leia e compreenda um programa, com o grau de confiança necessário para alterá-lo.
Também é desejável que a linguagem ofereça
facilidade de escrita de programas (redigibilidade).
Esta propriedade, entretanto, é colocada em outro plano, e é de certa forma conflitante com a
legibilidade.
Qualidades da Linguagem
(compilador ou interpretador)
Eficiência do programa compilado
Eficiência do processo de compilação
Disponibilidade de ferramentas
Disponibilidade de bibliotecas
Tradutores
Um tradutor é um programa que aceita como entrada um texto na linguagem fonte e gera um outro texto semanticamente equivalente escrito na linguagem objeto.
Exemplo:
Tradutor de chinês para inglês - Linguagens Naturais (IA)
Tradutor de Java para C
Tradutor de Java para x86
Um assembler x86 - Traduz a linguagem assembly x86 para código de máquina x86.
Assembler e Compilador
Um assembler traduz uma linguagem assembly para o código de máquina correspondente, gerando uma instrução em código de máquina por instrução de entrada.
Um compilador traduz uma linguagem de alto nível para um linguagem de baixo nível, gerando diversas instruções de código de máquina por comando de entrada.
Tipos de Processadores de Linguagens
Assembler e Compilador são os tipos de tradutores mais conhecidos, mas existem também:
Tradutores de alto nível (linguagem fonte e objeto são de alto nível). Ex: Java - C
Disassembler (código de máquina para linguagem assembly)
Decompiler (linguagem de baixo nível para linguagem de alto nível)
Linguagens
Tradutores e outros processadores de linguagens são programas que manipulam programas
Diferentes linguagens são envolvidas: a linguagem fonte, a linguagem objeto e a linguagem na qual o tradutor foi desenvolvido, chamada linguagem de implementação
Diagramas tombstone
Serão usados para representar programas e
processadores de linguagens, e para expressar a manipulação de programas por processadores.
Um programa é representado por:
onde o programa P é expresso em uma linguagem L.
L
P
Diagramas tombstone
Exemplos de diagramas representando programas:
x86 sort Java
sort
Basic
graph
Diagramas tombstone
Todo programa roda em uma máquina. Uma máquina que executa código de máquina M e representada por:
Exemplos de diagramas representando máquinas:
M
PPC
x86
SPARCDiagramas tombstone
Um programa pode rodar em uma máquina se é expresso em código de máquina adequado:
M
P
M
Diagramas tombstone
Exemplos de diagramas representando execuções de programas:
Um desses diagramas tem um erro. Qual?
x86 sort x86
PPC sort PPC
PPC sort
x86
Diagramas tombstone
Diagrama representando um tradutor S - T expresso em linguagem L:
onde S é linguagem fonte, T é a linguagem objeto e L a linguagem de implementação.
S → T
L
Diagramas tombstone
Exemplos de diagramas representando tradutores:
Observando os exemplos anteriores, separe-os em compilador, tradutor de alto nível e montador.
Java → x86 C
Java → x86 x86
Java → C C++
x86 ass. → x86
x86
S → T M S
P
M
T P
Deve ser igual Deve ser igual
Diagramas tombstone
Tradução de um programa fonte P expresso em linguagem S para um programa objeto expresso em linguagem T, usando um tradutor S - T que roda em uma máquina M.
Diagramas tombstone
O que representa esse diagrama?
Java → x86 x86
Java sort
x86
x86 sort
x86
sort
x86
Cross-Compiler
Quando utilizamos?
Java → PPC x86
Java sort
x86
PPC sort
PPC sort
download
PPC
Comportamento de um tradutor
Pode rodar em uma máquina M se for expresso em código de máquina M
O programa fonte deve ser expresso na linguagem fonte S do tradutor
O programa objeto deve ser expresso na linguagem objeto T do tradutor
O programa objeto é semanticamente equivalente ao programa fonte.
Passos ilegais
C → x86 x86
Java sort
x86
Java → x86 x86
Java sort
PPC
Compilação em dois passos
Um tradutor expresso em C ou Java pode rodar em qualquer máquina?
O que podemos fazer?
Tradução em dois passos.
Java → C x86
Java sort
x86
C sort
C → x86 x86
x86
sort
x86
Compilando um compilador
C → x86 x86 x86 Java → x86
C
Java → x86
x86
Interpretadores
Em um ambiente interativo, interpretação é um método interessante de trabalho.
Exemplos: interpretador Basic, interpretador Lisp, shell do UNIX e interpretador SQL
Vantagens:
Se o programador trabalha em um modo interativo, e deseja ver os resultados de cada instrução antes de entrar com a próxima instrução.
Se o programa é utilizado uma vez e depois é descartado, portanto a velocidade não é tão importante.
Se cada instrução for executada uma única vez
Se as instruções têm um formato simples, podendo ser analisadas facil e eficientemente
Interpretadores
Desvantagem:
A interpretação de um programa fonte em uma linguagem de alto nível é 100 vezes mais lenta que a execução do programa equivalente em código de máquina.
Não é adequada quando:
O programa deve ser executado em produção (velocidade é importante)
As instruções devem ser executadas freqüentemente
As instruções estão em formatos mais complicados
(linguagens de alto nível) e portanto é necessário mais tempo para análise.
Diagrama tombstone
Interpretador S expresso em uma linguagem L
Exemplos de diagramas representado interpretadores:
S L
SQL x86
shell C Basic
x86
shell
SPARC
Diagramas tombstone
S
P
S
M M
Diagramas tombstone
Qual dos diagramas está incorreto?
Lisp chess
Lisp x86 x86 Basic
graph Basic
x86 x86
Lisp chess
Basic
x86 x86
Máquinas Reais e Abstratas
Suponha que um engenheiro de computação
projetou um conjunto de instruções e a arquitetura de uma nova máquina, chamada Ultima.
Construir Ultima pode ser caro e tomar tempo.
Modificar o hardware para implementar mudanças de projeto também pode ter um alto preço.
É possível que o engenheiro adie a construção do hardware até que tenha como testar o projeto?
Ele pode escrever um interpretador para código de máquina Ultima (em C, por ex.).
Máquinas Reais e Abstratas
Tradução de um interpretador para código de máquina M, usando o compilador C
C → M M
M
Ultima C
Ultima
M
Máquinas Reais e Abstratas
Ultima P
Ultima M M
Ultima Ultima
P
Mesma funcionalidade
Máquinas Reais e Abstratas
A velocidade é a mesma utilizando essa alternativa?
Não pode ser usado para medir a velocidade
absoluta da máquina sendo emulada, mas pode ser usado para obter informações quantitativas como:
ciclos de contagem de memória, grau de paralelismo, e outras.
Informações qualitativas também podem ser obtidas, como: quanto a arquitetura e o conjunto de
instruções satisfazem a necessidade dos programadores.
Esse tipo de interpretador é chamado de emulador.
Interpretive Compiler
JVM-code é uma linguagem intermediária orientada a Java.
Fornece instruções poderosas que correspondem diretamente à operações Java, tais como: criação de objeto, chamada de métodos, e indexação de vetores.
Portanto a tradução de Java para JVM-code é fácil e rápida.
Instruções em JVM-code, embora poderosas,
possuem um formato simples como instruções em código de máquina, e fáceis de analisar.
A interpretação de JVM-code é relativamente rápida:
apenas 10 vezes mais lenta que em código de máquina.
Interpretive Compiler
JDK consiste de um tradutor de Java para JVM-code e de um interpretador de JVM-code, que rodam em uma máquina M
Java → JVM M
Java P
M
JVM
P JVM
P JVM
M M
Portabilidade
O que significa portabilidade de um programa?
Facilidade de executar o programa em qualquer máquina sem necessidade de alterações.
A linguagem na qual o programa é escrito tem grande impacto na portabilidade
Um programa escrito em Assembly não pode ser executado em outra máquina sem que seja
totalmente refeito.
Um programa escrito em linguagem de alto nível é bastante portável. Só é necessário recompilá-lo para executá-lo em outra máquina.
Compiladores Portáveis
Um processador de linguagem é um programa,
dessa forma, também é importante que seja portável.
A maioria dos processadores de linguagens são escritos em linguagens de alto nível.
Se temos um compilador escrito em linguagem de alto nível que compila código C para x86, temos ainda um problema, esse compilador pode ser alterado para uma outra máquina que não aceita código x86?
Um compilador que gera linguagem intermediária é mais portável que um que gera código de máquina.
Exemplo
JVM M
Java → JVM JVM
M Java
P
JVM P
JVM P JVM
M M
O interpretador de JVM-code é mais simples e menor que o compilador, dessa forma, escrevê-lo
novamente é uma tarefa mais fácil
Bootstrapping
Quando a linguagem de implementação é a
linguagem fonte, o processador pode ser usado para processar ele mesmo.
Um compilador portável pode ser obtido com a
técnica de bootstrap, resultando em um compilador de fato - que gera código de máquina.
Primeiramente, escrevemos um tradutor de JVM- code para código de máquina M, em Java:
JVM → M
Java
Bootstrapping um Compilador Portável
Java → JVM JVM
JVM → M Java
JVM → M JVM
JVM M
M
Bootstrapping um Compilador Portável
JVM → M JVM
JVM → M JVM
JVM → M M
JVM M
M
Bootstrap
Bootstrapping um Compilador Portável
JVM → M M
Java → JVM JVM
Java → JVM M
M
Bootstrapping um Compilador Portável
Java → JVM M
P Java → JVM
M Java
P
M
JVM P
JVM → M M
M
Bootstrap Total
Para que um programa seja portável ele deve ser escrito em uma linguagem de alto nível (L)
Caso seja preciso gerar uma nova versão do
programa (corrigir algum problema, melhorá-lo, etc) será necessário editá-lo e recompilá-lo.
Dessa forma, o programa é mantido enquanto houver um compilador disponível.
Bootstrap Total
Para um compilador escrito em L, o mesmo problema pode ocorrer
Exemplo:
C → x86 x86 x86 Java → x86
C
Java → x86
x86
Bootstrap Total
Um compilador cuja linguagem fonte é S, expresso em linguagem L, só pode existir enquanto houver um compilador para L.
Esse problema pode ser evitado usando a linguagem S para escrever o compilador.
Caso seja necessário construir uma nova versão do compilador S, podemos utilizar o compilador
existente para compilar a nova versão.
Bootstrap Total
O problema está em como compilar a primeira versão do compilador S.
A idéia utilizada para resolver este problema é usar um subconjunto de S adequado para a escrita do primeiro compilador.
Suponha que desejamos desenvolver um compilador Ada para código de máquina M:
Bootstrap Total
Ada-S → M C
v1
M
Ada-S → M M
v1
M
C → M
Bootstrap Total
Podemos utilizar o compilador resultante para testar programas escritos em Ada-S
Entretanto, o compilador poderá ser mantido enquanto tivermos um o compilador C
Dessa forma, podemos desenvolver o compilador usando a própria linguagem Ada-S
Ada-S → M Ada-S
v2
Ada-S → M M
Ada-S → M M
v2
M
v1
Bootstrap Total
Com isso podemos compilar e testar os programas em Ada-S sem precisar do compilador C
Finalmente, modificamos o compilador Ada-S para compilar programas em Ada
Ada → M Ada-S
v3
Ada-S → M M
Ada → M M
v3
M
v2
Bootstrap Parcial
Suponha que exista um compilador que roda em uma máquina HM (host machine) e desejamos
utilizar este compilador em uma outra máquina TM (target machine).
Para que esse compilador possa gerar código de máquina TM é necessário alterar grande parte do compilador, que é responsável pela geração de código (50%).
Se o compilador é expresso utilizando sua própria linguagem fonte, estamos falando do processo de Bootstrap Parcial.
Bootstrap Parcial
Ada → HM Ada
Ada → HM HM
Supondo que temos os seguintes tradutores:
E desejamos um compilador que roda na máquina TM e gera código de máquina TM
Primeiramente, modificamos o gerador de código para gerar código de máquina TM
Bootstrap Parcial
Ada → TM
Ada
Produzimos assim um cross-compiler. Como podemos testá-lo?
Bootstrap Parcial
Ada → TM
Ada Ada → HM HM
Ada → TM HM
HM
Bootstrap Parcial
Ada → TM HM
Ada P
HM
TM P
TM P
download
TM
Após testar o compilador gerado podemos utilizá-lo para compilar ele mesmo, de forma que gere código de máquina TM.
Bootstrap Parcial
Ada → TM
Ada Ada → TM HM
Ada → TM TM
HM
Bootstrapping para melhorar a eficiência
O que pode ser usado para medir a eficiência de um programa?
Quando estamos avaliando um compilador, o que deve ser considerado?
Eficiência do próprio compilador
Eficiência dos programas que ele gera
Bootstrapping para melhorar a eficiência
Ada → M
slowAda
Ada → M
slowM
slowv1 v1
Suponha que tenhamos:
Modificamos a versão 1 para que gere código de máquina Mfast
Ada → M
fastAda
v2
Bootstrapping para melhorar a eficiência
Ada → M
fastAda Ada → M
slowM
slowAda → M
fastM
slowM
v2 v2
v1
Bootstrapping para melhorar a eficiência
Ada → M
fastM
slowAda P
M
M
fastP
M
fastP M
v2
Bootstrapping para melhorar a eficiência
Ada → M
fastAda Ada → M
fastM
slowAda → M
fastM
fastM
v2 v3
v2
Bootstrapping para melhorar a eficiência
Ada → M
fastM
fastAda P
M
M
fastP
M
fastP M
v3