CES-41 Teoria Cap 1-b
Download
Report
Transcript CES-41 Teoria Cap 1-b
1.3 – Interpretadores
1.3.1 – Compiladores versus Interpretadores
Execução de um programa gerado por compilador:
Dados
Programa
fonte
Compilador
+
Montador
Programa em
linguagem
de máquina
Em tempo de
compilação
Em tempo de
execução
Resultados
Execução de um programa interpretado:
O interpretador faz análise léxica, sintática e semântica do
programa-fonte, armazenando-o numa estrutura interna
Em seguida, ele percorre a estrutura interna, executando
as operações ali especificadas, consumindo os dados por
elas pedidos
Dados
Interpretador
Programa
fonte
Módulo de
análise e
armazenamento
Programa
numa
estrutura
interna
Módulo
de
execução
Em Tempo de execução
Resultados
Execução de um programa interpretado:
Não há criação de um programa em linguagem de
máquina equivalente ao programa-fonte
O único programa executado é o interpretador
Dados
Interpretador
Programa
fonte
Módulo de
análise e
armazenamento
Programa
numa
estrutura
interna
Módulo
de
execução
Em Tempo de execução
Resultados
Exemplo: seja o comando de atribuição
v1 = v2 + v3 * v4;
Um interpretador pode construir-lhe uma árvore de
execução (estrutura interna):
=
Depois ele caminha pela árvore,
executando as operações
Na raiz, ele faz uma atribuição da expressão do lado direito à
variável do lado esquerdo
No cálculo dessa expressão, ele soma a variável do lado
esquerdo com a expressão do lado direito (chamada
recursiva do cálculo de expressão)
=
Há interpretadores de código fonte e de código
intermediário
Interpretador de código fonte: Deve haver uma forma de
armazenamento do programa fonte
Interpretador de código intermediário: o código
intermediário pode ser gerado pelo mesmo processo usado
nos compiladores
1.3.2 – Interpretadores de código-fonte
Durante a análise sintática, o programa-fonte é
armazenado numa estrutura denominada árvore do
programa
Depois, essa estrutura é percorrida, para que os comandos do
programa sejam executados
Exemplo: programa para o Bubble-Sort
void main () {
int n, i, p, aux, vetor[50];
char trocou;
read (n);
for (i=0; i<n; i++)
read (vetor[i]);
trocou = 1;
for (p = n-2; p>=0 && trocou == 1; p--) {
trocou = 0;
for (i = 0; i<=p; i++)
if (vetor[i] > vetor[i+1]) {
aux = vetor[i];
vetor[i] = vetor[i+1];
vetor[i+1] = aux;
trocou = 1;
}
}
for (i=0; i<n; i++)
write (vetor[i]);
}
Sua árvore de
armazenamento
As declarações
podem ser
eliminadas
Cada nó retangular
pode ser mais
detalhado
Cada nó retangular
pode ser mais
detalhado
Ver
detalhes
Cada nome de
variável deve ser
substituído por
um ponteiro para
sua posição na
tabela de símbolos
Lá deve haver um
campo para
guardar seu valor
Interpretação de
código fonte é
muito ineficiente
Os programas
costumam ter
muitos
aninhamentos
Cada tipo de
comando é
executado por um
módulo específico
O módulo de um
comando
repetitivo irá
chamar os
módulos dos
comandos de seu
escopo
Se um deles for
um comando
condicional,
ocorrerá com ele o
mesmo
Num dado
momento,
poderão estar
ativas várias
versões de
diversos módulos
Há consumo de muita memória e sobrecarga de trabalho
para gerenciá-la
Os interpretadores de código intermediário são os
preferidos
No entanto, o conceito de árvore de programa é muito útil
em softwares que fazem análise do programa-fonte
Por exemplo, em compiladores paralelos, a detecção de
paralelismo exige análise de dependências que só pode ser
feita se o programa-fonte estiver apropriadamente
armazenado
1.3.3 – Interpretadores de código intermediário
Podem ter os mesmos componentes da frente de um
compilador convencional:
Analisadores léxico, sintático e semântico
Gerador de código intermediário
Otimizador de código intermediário
Além desses, devem ter o componente que vai fazer a
interpretação propriamente dita do código intermediário
Esse componente percorre o código, executando as
operações ali especificadas e processando os dados por elas
solicitados
Exemplo: laço do programa anterior:
for (i = 0; i<=p; i++)
if (vetor[i] > vetor[i+1]) {
aux = vetor[i]; vetor[i] = vetor[i+1];
vetor[i+1] = aux; trocou = 1;
}
Suas quádruplas com alguma otimização:
A propósito, quádruplas para indexação:
Sejam as seguintes declarações e comandos:
int i, j, k, A[6][5];
i = 4; j = 3;
k = A[i][j-2];
A[10-i][2*j+3] = i + j * k;
Acesso a um elemento genérico A[i][j]:
Uma vez conhecido o endereço inicial da matriz A, é
necessário localizar o elemento A[i][j]
Seja a seguir o mapa de A[6][5] na memória:
Seja m o número de linhas e n o
número de colunas da matriz A
O endereço do elemento A[i][j] é
dado pela fórmula:
Ender (A) + i * n + j
Para m = 6, n = 5, o endereço de
A[4][3] é
Ender (A) + 23
No programa, cada índice pode
ser uma expressão inteira
Calcula-se o valor de cada índice,
empilhando-o numa pilha de
índices
Isso pode ser feito pela execução
de uma quádrupla de operador
IND:
IND, i , ---- , ---IND, j , ---- , ----
Calcula-se o endereço de A[i][j],
usando uma quádrupla de
operador INDEX:
INDEX , A , 2 , temp1
Sua execução consiste em:
Pegar as dimensões e o
endereço de A na tabela de
símbolos
Desempilhar dois índices
Calcular o endereço,
colocando-o na variável temp1
A variável temp1 é portanto um
ponteiro
Formação do código intermediário:
int i, j, k, A[6][5];
i = 4; j = 3;
k = A[i][j-2] + 5;
A[10-i][2*j+3] = i+j*k;
temp2 tem o endereço do
elemento A[i][j-2]
É necessário saber o valor
guardado nesse elemento
A quádrupla
@, temp2, ..., temp3
atribui a temp3 o valor do local
apontado por temp2
:=, 4, ..., i
:=, 3, ..., j
IND, i, ..., ...
-, j, 2, temp1
IND, temp1, ..., ...
INDEX, A, 2, temp2
@, temp2, ..., temp3
+, temp3, 5, k
Formação do código intermediário:
int i, j, k, A[6][5];
i = 4; j = 3;
k = A[i][j-2] + 5;
A[10-i][2*j+3] = i+j*k;
:=, 4, ..., i
:=, 3, ..., j
IND, i, ..., ...
-, j, 2, temp1
IND, temp1, ..., ...
INDEX, A, 2, temp2
temp7 tem o endereço do
elemento A[10-i][2*j+3]
@, temp2, ..., temp3
+, temp3, 5, k
O valor de temp9 deve ser
atribuído ao local apontado por
temp7
-, 10, i, temp4
IND, temp4, ..., ...
*, 2, j, temp5
+, temp5, 3, temp6
IND, temp6, ..., ...
INDEX, A, 2, temp7
A quádrupla
#, temp9, ..., temp7
atribui o valor de temp9 ao local
apontado por temp7
*, j, k, temp8
+, i, temp8, temp9
#, temp9, ..., temp7
Voltando ao laço do programa anterior:
for (i = 0; i<=p; i++)
if (vetor[i] > vetor[i+1]) {
aux = vetor[i]; vetor[i] = vetor[i+1];
vetor[i+1] = aux; trocou = 1;
}
Suas quádruplas:
Estrutura de
dados
São
omitidos
vários
ponteiros
As
quádruplas
poderiam
ser
guardadas
numa lista
encadeada
em vez de
num vetor
Desvantagens da interpretação:
A execução de programas compilados é muito mais rápida
que a de programas interpretados
Apesar do código intermediário ser aperfeiçoado antes da
interpretação propriamente dita, o código objeto ainda pode
ser otimizado
Isso não ocorre em programas interpretados
Um interpretador faz por software a interpretação do
código da operação; num programa compilado isso ocorre
por hardware, o que é muito mais rápido
Vantagens da interpretação:
O código intermediário é independente de máquina, o que
lhe confere portabilidade
Um programa compilado só pode rodar em máquinas
compatíveis com sua máquina alvo
Interpretadores se mostram adequados para as redes de
computadores heterogêneos, que é o caso da Internet
A elaboração de um módulo interpretador propriamente
dito é muito mais simples que a de uma retaguarda de
compilador
Os detalhes da arquitetura da máquina-alvo complicam
muito o projeto da retaguarda
Para casos em que a rapidez de execução não é
fundamental, os interpretadores são preferidos, em
comparação com os compiladores
A Linguagem Java:
Seu compilador gera código denominado bytecode,
independente de máquina
Bytecode é traduzido para linguagem de máquina por uma
máquina virtual residente em qualquer ambiente Java
Bytecode é portável, porém, para ser executado, deve ser
traduzido, o que torna o processo mais lento
1.4 – Automação da Construção de
Compiladores
1.4.1 – Bootstrapping e compiladores cruzados
O compilador para a primeira linguagem de programação só
poderia ter sido escrito em Assembly
Assim foi com Fortran e Cobol
Seus projetos demandaram esforço de programação
descomunal
No princípio:
Assembler
Máquina M1
Primeiro compilador Fortran, escrito em Assembly:
Fortran
em Assembly
Assembler
Fortran
em M1
Máquina M1
Compilador Algol, escrito em Fortran:
Fortran
em Assembly
Assembler
Algol
em Fortran
Fortran
em M1
Máquina M1
Algol
em M1
Compilador Pascal, escrito em Algol:
Fortran
em Assembly
Pascal
em Algol
Assembler
Algol
em Fortran
Fortran
em M1
Máquina M1
Algol
em M1
Pascal
em M1
Compilador C, escrito em Pascal:
Fortran
em Assembly
Pascal
em Algol
C
em Pascal
Assembler
Algol
em Fortran
Fortran
em M1
Algol
em M1
C
em M1
Máquina M1
Pascal
em M1
Em UNIX, compiladores L1, L2, ... , escritos em C:
Fortran
em Assembly
Pascal
em Algol
C
em Pascal
Assembler
Algol
em Fortran
L1
Fortran
em M1
Algol
em M1
em C
L2
C
em C
em M1
Máquina M1
Pascal
em M1
L1
em M1
L2
em M1
Em UNIX, compiladores C eram escritos em C:
Fortran
em Assembly
Pascal
em Algol
C
em Pascal
Assembler
Algol
em Fortran
C1
em C
C2
em C
Fortran
em M1
Algol
em M1
C
em M1
Máquina M1
Pascal
em M1
C1
em M1
C2
em M1
Bootstrapping: propriedade de uma linguagem compilar a si
mesma
Compilador cruzado: compilador que roda numa máquina e
gera código para outra
Para uma máquina M2, seria necessário escrever um programa
em Assembly para um primeiro compilador?
Com bootstrapping e compiladores cruzados pode-se
evitar isso (visto a seguir)
Um compilador é caracterizado por 3 linguagens:
A linguagem-fonte (F) que ele compila
A linguagem-objeto (O) para a qual ele gera código
A linguagem de implementação (I) na qual ele está
escrito
Simbolicamente, FIO
Ou, usando diagrama T:
F
O
I
Seja M usado para denotar a linguagem de máquina de um
computador ou máquina M
Para um compilador rodar na máquina M, sua linguagem de
implementação deve ser M
F
O
M
Seja um compilador L1MM residindo na máquina M
Deseja-se em M um compilador para uma nova linguagem L2,
ou seja, L2MM
Primeiramente escreve-se um programa L2L1M
Depois roda-se na máquina M:
Entrada
Programa em
execução
L2
M
L1
L2
M
Saída
L1
M M
M
O compilador de
L2 está pronto
para rodar em M
Esquema para se obter um compilador cruzado:
Entrada
Compilador
residindo em M1,
em execução
L2
M2
L1 L1
L2
M2
M1 M1
M1
Saída
Agora em M1, um compilador de L2 para a máquina M2
Os programas escritos em L2 são compilados em M1 e o
código objeto é transportado para M2
Seja então L a primeira linguagem a ser instalada na máquina
M2, mas já instalada na máquina M1 (LM1M1)
Primeiramente, bootstrapping: LLM2
Depois, produz-se em M1 um compilador cruzado
Finalmente, usando em M1 o compilador cruzado produzido
É só transportar LM2M2 para a máquina M2
Entrada
Entrada
Programa em
execução
Programa
em
execução
L
M1 M1
L
M1
L
M2
L
L
M2
M2 M2 Saída
Saída
Compilador
desejado
1.4.2 – Compiladores de compiladores
As ferramentas de automação, logo que começaram a surgir,
receberam alguns nomes um tanto ambiciosos:
Geradores de compiladores
Compiladores de compiladores
Sistemas de construção de tradutores
1.4.2 – Compiladores de compiladores
Eram ferramentas de uso limitado, pois eram orientadas em
torno de modelos particulares de linguagens
Devido à grande heterogeneidade das linguagens e das
arquiteturas, é muito difícil a elaboração de um gerador de
propósitos gerais eficiente
O que existe hoje são ferramentas automáticas para o
projeto de componentes específicos, relacionados a seguir
1.4.3 – Ferramentas para cada componente
Utilizam linguagens especializadas para a especificação e
implementação do componente e algoritmos bem sofisticados
Geradores de analisadores léxicos:
Têm como entrada expressões regulares e implementam um
autômato finito reconhecedor e classificador dos átomos dos
programas a serem compilados
A mais conhecida entre elas é o Lex do sistema Unix, que
possui também versões para o sistema DOS
O programa gerado é escrito em Linguagem C
1.4.3 – Ferramentas para cada componente
Geradores de analisadores sintáticos:
Têm como entrada a gramática livre de contexto da
linguagem-fonte do compilador
Nos compiladores primitivos, a análise sintática consumia
grande fração do tempo de compilação e do esforço
intelectual para escrever um compilador
Hoje é considerada uma das fases mais fáceis de serem
implementadas
1.4.3 – Ferramentas para cada componente
Geradores de analisadores sintáticos:
Utilizam algoritmos de analise muito eficientes, porém muito
complexos para serem implementados à mão
A mais conhecida: Yacc (Yet Another Compiler-Compiler)
do sistema UNIX que também possui diversas versões para o
sistema DOS
O programa gerado também é escrito em Linguagem C
1.4.3 – Ferramentas para cada componente
Geradores de código intermediário:
Produzem uma coleção de rotinas que, ao caminhar pela
árvore sintática do programa, já com atributos calculados
pelo analisador semântico, produzem o código intermediário
Analisadores de fluxo de dados:
Importante ferramenta para a otimização do código
intermediário
1.4.3 – Ferramentas para cada componente
Geradores de código objeto:
Recebem como entrada uma coleção de regras que definem
a tradução de cada tipo de comando do código
intermediário em código de máquina ou Assembly
Essas regras devem incluir detalhes suficientes para se
escolher os locais adequados para alocação de variáveis
(registradores, memória, pilha, etc)