Compiladores
Verificação de tipos –
Suporte ao runtime
Análise Semântica e
checagem de tipos
• Introdução: o que são tipos, para que servem...? – Representação de tipos
• Exemplo de verificação de tipos: – Uma linguagem simples
• Declarações, expressões, instruções, funções...
– Uso de regras semânticas para verificar a declaração de tipos. – Uso de regras semânticas para verificar o tipo de expressões – Uso de regras semânticas para verificar o tipo de comandos – Uso de regras semânticas para verificar o tipo de funções
Tipos
•
Definições:
– “Um tipo é uma coleção de valores computáveis que compartilham alguma propriedade estrutural” (Mitchell)
– “Uma coleção de valores que um fragmento de programa pode assumir durante execução” (Cardelli)
•
Porque ter tipos?
1. Se voce possui tipos e um sistema tipado (conjunto de regras), é possível checar em
pré-processamento para evitar erros de execução 2. Estrutura do programa & documentação 3. Manutenção & engenharia de software 4. Otimizações
Propriedades de Sistemas Tipados
• Uma linguagem tipada irá retornar um erro caso hajainconsistência entre os tipos dos dados.
– Programador deve saber porque o programa não compila ou produz erro
• Verificação é não trivial: – Linguagens fortemente tipadas
• Nenhum erro de tipo passa despercebido • Exemplo: ML, ADA, Pascal (maior parte), Java
– Linguagens fracamente tipadas
• Erros de tipos podem ocorrer:
• C/C++: usando conversão, aritmética envolvendo ponteiros, passando final de um array, etc
• Balanço:
– Linguagens fracamente checadas precisam de alguma forma de “garbage collection” (podem impactar o tempo de execução) – Mas são mais fáceis de escrever programas (vantagem sobre
aspecto de tempo de desenvolvimento)
Regras semânticas
•
Exemplos:
– Se ambos operandos de operações aritméticas são inteiros, então o resultado é inteiro
– O resultado de um operador unário & é um ponteiro para o objeto descrito pelo operando
•
Denotar o tipo de uma construção de
linguagem:
– Expressão de Tipo: •tipo básico •tipo estruturado:
– formado ou formado através da aplicação de um operador de
Expressões de tipos
1. Um tipo básico é um expressão de tipo.
Ex: boolean, char, integer, real, etc. Um tipo especial, type_error sinaliza um erro durante checagem
2. Se tipos de expressões podem ter nomes, um nome de tipo é uma expressão de tipo.
Ex. Registros/struct
3. Construtores de tipos aplicados a tipos de expressões são expressões de tipos
Array, ponteiro
4. Expressões de Tipos podem conter variáveis cujos valores são expressões de tipos
Construtores de tipos
1. Arranjos:
Se T é um expressão de tipo, então array (I, T) é uma expressão de tipo.
Ex: var A: array[1..10] of integer;
2. Produtos:
Se T1 e T2 são expressões de tipos, então o produto cartesiano T1 x T2 é uma expressão de tipo.
3. Registros:
Um registro é formado por campos com nomes
4. Ponteiros:
Se T é uma expressão de tipo, então ponteiro(T) representa o tipo relativo ao ponteiro de um objeto de tipo T
5. Funções:
Mapeamento de um tipo de domínio D em um intervalo de domínio R.
Ex: function f(a, b: char) : pointer to integer char x char -> pointer(integer)
Representação Gráfica
x char char pointer integer →function f(a, b: char) : pointer to integer
Representação de tipos estruturados
struct s1 {
int large1;
short int small1;
};
struct s2 {
int large2 : 18;
short int small2 : 10;
};
32 16 16 18 10 4
large1 small1 large2 small2
Checagem compilação vs. execução
•
Checagem dinâmica x estática
•
Estática: em tempo de compilação
– Antes da execução (“para todas as entradas”)
– Compilador maior, restringe flexibilidade,
menos expressiva
•
Dinâmica: em tempo de execução
– Durante a execução (“com uma dada
entrada”)
– Pode ser muito tarde!
– Cara, mas mais flexível
Regras semânticas - declaração
•
P -> D ; E
•
D -> D ; D
•
D -> id : T
{addtype(id.entry, T.type) }
•
T -> char
{ T.type := char }
•
T -> integer
{ T.type := integer }
•
T ->
↑
T
1{T.type := pointer(T
1.type) }
•
T
→
array [num] of T
1Exemplo de checagem de Tipos
•
Uma linguagem de Programação simples:
– Variáveis são definidas antes de serem usadas – Semelhante a Pascal
N :integer; N mod 1999
•
P -> D ; E
•
D -> D ; D | id : T
•
T -> char | integer | array [ num ] of T |
↑
T
•
E -> literal | num | id | E mod E | E [E] | E
↑
Regras semânticas - declaração
•
P -> D ; E
•
D -> D ; D
•
D -> id : T
{addtype(id.entry, T.type) }
•
T -> char
{ T.type := char }
•
T -> integer
{ T.type := integer }
•
T ->
↑
T
1{T.type := pointer(T
1.type) }
•
T
→
array [num] of T
1{ T.type :=
array(1..num.val,
T
1.tipo)}
Verificação de Tipos de Expressões
•
E -> literal
{ E.type := char }
•
E -> num
{ E.type := integer }
•
E -> id
{ E.type := lookup(id.entry) }
•
E -> E1 mod E2
{ E.type :=if E1.type == integer and E2.type = integer then integer else type_error}
•
E -> E1 [E2]
{ E.type := if E2.type == integer and E1.type == array(s, t) then t else type_error }•
E -> E
↑
{E.type := if E1.type == pointer(t) then telse type_error }
Conversões Implícitas
E.tipo := IF E1.tipo = inteiro e E2.tipo = inteiro
THEN inteiro
ELSE IF E1.tipo = real e E2.tipo = real
THEN real
ELSE IF E1.tipo = real e E2.tipo = inteiro
THEN real
ELSE IF E1.tipo = inteiro e E2.tipo = real
THEN real E →E1op E2 E.tipo := procurar(id.entrada) E →id E.tipo := real E →num.num E.tipo := inteiro E →num
REGRA SEMÂNTICA
PRODUÇÃO
OBS: Resolvidas em tempo de compilação
Conversões Implícitas e Desempenho
•
Pode melhorar tempo de execução
– Observação de Bentley [1982] em Pascal
– Com X sendo um array de reais:
•for i := 1 to n do X[i] := 1 – Leva 48,4 x n micro-secundos
•for i := 1 to n do X[i] := 1.0 – Leva 5,4 x n micro-secundos
– Compiladores inteligentes convertem 1 para
1.0 em tempo de compilação
Compiladores
Ambiente de suporte à
execução
Suporte à Execução
•
O Compilador gera código executável.
•
Mas nem tudo está conhecido antes que o
programa seja executado!
– Valores de parâmetros e funções, – Memória dinamicamente alocada,– Dependendo do número de chamadas, qual endereço usar para achar o início de cada execução de um procedimento?
– Etc...
•
É preciso de um conjunto de rotinas (run-time
support package) carregado junto com o código
objeto gerado
Procedimentos
•
Os comandos são organizados em
procedimentos:
– declaração com um nome associado que
realiza uma dada tarefa
– definição:
•nome
•variáveis
•corpo
Procedimentos em ação (ativação)
•
Fluxo de controle:
– execução seqüencial (seqüência de passos)
– começa no início do corpo
– termina no final do corpo
– tempo de vida:
•seqüência de passos executados
– chamada de procedimentos:
•desvio de execução
•retorna o controle para o ponto imediatamente após o ponto de chamada
Exemplo: o Quicksort
program sort(input, output);var a : array [0..10] of integer; procedure readarray;
var i: integer; begin
for i:=1 to 9 do read(a[i]); end;
function partition(y, z: integer): integer; var i, j, x, v: integer; begin ... end; procedure quicksort(m, n: integer);
var i: integer; begin if ( n > m) then begin i:= partition(m, n); quicksort(m, i-1); quicksort(i+1, n); end; end; begin
a[0] := -9999; a[10]:= 9999; readarray; quicksort(1, 9);
end.
Árvores de Ativação
•
Cada nó representa uma ativação de um
procedimento
•
A raiz representa a ativação do programa
principal
•
O nó de ‘a’ é pai de ‘b’ se e somente se o fluxo
de controle muda de ‘a’ para ‘b’
•
O nó de ‘a’ está à esquerda de ‘b’ se e somente
se a vida de ‘a’ ocorre antes de ‘b’
Árvores de ativação
execution begins sÁrvores de ativação
execution begins enter readarray s rÁrvores de ativação
execution begins enter readarray leave readarray s rÁrvores de ativação
execution begins enter readarray leave readarray enter quicksort(1, 9) s r q(1,9)Árvores de ativação
execution begins enter readarray leave readarray enter quicksort(1, 9) enter partition(1, 9) s r q(1,9) p(1,9)Árvores de ativação
execution begins enter readarray leave readarray enter quicksort(1, 9) enter partition(1, 9) leave partition(1,9) s r q(1,9) p(1,9)Árvores de ativação
execution begins enter readarray leave readarray enter quicksort(1, 9) enter partition(1, 9) leave partition(1,9) enter quicksort(1,3) s r q(1,9) q(1,3) p(1,9)Árvores de ativação
execution begins enter readarray leave readarray enter quicksort(1, 9) enter partition(1, 9) leave partition(1,9) enter quicksort(1,3) ... leave quicksort(5, 9) s r q(1,9) q(1,3) p(1,3)q(1,0) q(2,3) p(2,3)q(2,1) q(3,3) p(7,9)q(7,7) q(9,9) p(5,9)q(5,5) q(7,9) q(5,9) p(1,9)Árvores de ativação
execution begins enter readarray leave readarray enter quicksort(1, 9) enter partition(1, 9) leave partition(1,9) enter quicksort(1,3) ... leave quicksort(5, 9) leave quicksort(1,9) s r q(1,9) q(1,3) p(1,3)q(1,0) q(2,3) p(2,3)q(2,1) q(3,3) p(7,9)q(7,7) q(9,9) p(5,9)q(5,5) q(7,9) q(5,9) p(1,9)Árvores de ativação
execution begins enter readarray leave readarray enter quicksort(1, 9) enter partition(1, 9) leave partition(1,9) enter quicksort(1,3) ... leave quicksort(5, 9) leave quicksort(1,9) execution terminated s r q(1,9) q(1,3) p(1,3)q(1,0) q(2,3) p(2,3)q(2,1) q(3,3) p(7,9)q(7,7) q(9,9) p(5,9)q(5,5) q(7,9) q(5,9) p(1,9)Pilhas de controle
•
O fluxo de controle
corresponde a uma
busca em
profundidade
na
árvore de ativação
•
Usa-se uma pilha
para controlar as
ativações de
procedimentos
ativos
s r q(1,9) q(1,3) p(1,3)q(1,0) q(2,3) p(1,9)Organização de Memória
•
Como a memória do programa é armazenada ?
– código objeto gerado; – espaço para variáveis globais
•área estática
– pilha para ativação de procedimentos – espaço para memória dinâmica (heap).
Núcleo
Texto
Pilha
HEAPAlocação de memória
•
Alocação estática
:
– reserva de memória é feita durante a compilação, de forma estática.
– Tipo (ou comprimento) do dado é conhecido em tempo de compilação
– Comprimento não é modificado durante a execução
do programa Área dados (SisOp)
Pilha
HEAP
Alocação de memória
•
Alocação dinâmica (HEAP)
:
– estruturas de dados referenciadas através de ponteiros, as áreas também são reservadas dinamicamente.
– áreas são alocadas e liberadas, sob o controle do programa
– alocadas na área de "heap", que cresce no sentido contrário ao da pilha
.
Área dados (SisOp)
Texto
Pilha
Alocação de memória
•
Alocação em pilha (STACK):
– Áreas para dados locais de procedimentos (subrotinas ou funções): devem ser alocadas dinamicamente.
– A alocação de espaço de memória somente pode ser realizada em tempo de execução, porque a ordem de chamadas é determinada pela execução do programa.
– Áreas são alocadas numa estrutura em pilha de ativação de procedimentos
– Na pilha entram (e saem) registros de ativação.
Registro de Ativação
aponta para dados não-locais, armazenados em outros registros de ativação valor retornado parâmetros efetivos ponteiro de controle ponteiro de acesso estado da CPU variáveis locais temporários
podem ser passados ou retornados em registradores (para maior eficiência)
"program counter"e registradores aponta para o registro de ativação da rotina chamadora
Obs: pode-se usar registradores para alguns dos campos!
Ponteiros de Acesso
parâmetros e valores retornados apontadores de controle e estado da máquina variáveis locais e temporáriosavaliados pela rotina chamadora
algumas informações são preenchidas pela rotina chamadora
área usada pela rotina chamada registro de ativação da rotina chamadora sp fp 2 ponteiros:
•frame_pointer (fp): aponta para o registro de ativação corrente • stack_pointer (sp): topo da pilha de registros de ativação
Código de Chamada
•
rotina chamadora
:
– avalia os parâmetros efetivos e os coloca na pilha – Registradores em uso pelo chamador são salvos em
memória
– armazena o endereço de retorno e o valor antigo do frame_pointer no registro de ativação da rotina chamada e atualiza o valor do frame_pointer;
•
rotina chamada
:
– salva valores de registradores e outras informações do estado da máquina;
– inicializa variáveis locais e começa sua execução.
Código de Retorno
•
rotina chamada:
– armazena o valor de retorno logo após o registro de ativação da rotina chamadora;
– restaura o apontador topo_ra e os registradores da máquina e desvia para o endereço de retorno dentro da rotina chamadora;
•
rotina chamadora:
– copia o valor retornado no seu próprio registro de ativação
Seqüência de chamada
valor retorno e parâmetrosPonteiros e Estado Tmp / dados locais
Tmp / dados locais valor retorno e parâmetros
Registro Ativação chamador
Registro Ativação chamado Topo
Topo
Seqüência de chamada
valor retorno e parâmetrosPonteiros e Estado Tmp / dados locais
Tmp / dados locais valor retorno e parâmetros
Ponteiros e Estado Topo Sob responsabilidade do chamador Sob responsabilidade do chamado
Seqüência de retorno
•
O chamado coloca um valor de retorno no
devido lugar...
– Ou seja, logo depois do registro do chamador!
•
Recupera o antigo valor de ‘topo’ no campo de
estado
– E também os outros registradores...
•
Desvia para o antigo PC
•
Importante: o chamador ainda pode acessar o
que está acima de ‘topo’
– ... Questão: o que está neste lugar?