compiladores - geração de código intermédiodesousa/2012-2013/lfc/geracodigo.pdftitle...

26
Compiladores Gera¸ ao de C´ odigo Interm´ edio vers˜ ao α - 0.002 Sim˜ ao Melo de Sousa Este documento ´ e a adapta¸ ao duma tradu¸ ao do cap´ ıtulo ”G´ en´ eration de Code In- term´ ediaire”da sebenta ”Cours de Compilation”de Christine Paulin-Morhing (http://www.lri.fr/~paulin). 1 Introdu¸ ao Antes de gerar um c´ odigo/programa particular, ´ e necess´ ario apresentar os problemas que se colocam ao organizar os dados e as instru¸ oes dentro dum programa. 1.1 Modelos de execu¸ ao Aquando da execu¸ ao de c´ odigo, uma parte da mem´ oria ´ e reservada para as instru¸ oes do programa com um apontador para indicar em que ponto (do programa) se encontra a execu¸ ao. O tamanho do c´ odigo ´ e conhecido na altura da compila¸ ao. Sup˜ oe-se que existe um controlo sequencial da execu¸ ao dum programa. Isto ´ e, que todas as instru¸ oes dum programa, excepto instru¸ oes de salto, s˜ ao executadas sequencialmente umas ap´ os a outra. Os dados s˜ ao guardados na mem´ oria ou nos registos da m´ aquina (que executa). A mem´ oria est´ a organizada em palavras. Esta ´ e acedida via um endere¸ co que ´ e representado por um inteiro. Os valores simples s˜ ao guardados numa unidade de mem´ oria, os valores complexos ou compostos (vectores, estruturas) s˜ ao guardados em c´ elulas cont´ ıguas da mem´ oria ou em estruturas encadeadas (uma lista poder´ a ser representada por um elementos associado a um apontador contendo o endere¸ co mem´ oria do pr´ oximo elemento). Os registos permitam o acesso r´ apido a dados simples. O n´ umero de registos depende da arquitectura alvo e ´ e em geral relativamente pequeno. 1.2 Aloca¸ ao Certos dados s˜ ao explicitamente manipulados pelo programa fontes pelo interm´ edio de vari´ aveis. Outro s˜ ao criados pelo compilador, por exemplo para conservar os valores de alculos interm´ edios ou para guardar ambientes associados a valores funcionais. Alguns dados tem uma dura¸ ao de vida conhecida na altura da compila¸ ao. ´ E assim poss´ ıvel, e at´ e desej´ avel, reutilizar o espa¸ co utilizado. A an´ alise da dura¸ ao de vida 1

Upload: others

Post on 26-Feb-2021

1 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

CompiladoresGeracao de Codigo Intermedio

versao α− 0.002

Simao Melo de Sousa

Este documento e a adaptacao duma traducao do capıtulo ”Generation de Code In-termediaire”da sebenta ”Cours de Compilation” de Christine Paulin-Morhing(http://www.lri.fr/~paulin).

1 Introducao

Antes de gerar um codigo/programa particular, e necessario apresentar os problemas quese colocam ao organizar os dados e as instrucoes dentro dum programa.

1.1 Modelos de execucao

Aquando da execucao de codigo, uma parte da memoria e reservada para as instrucoesdo programa com um apontador para indicar em que ponto (do programa) se encontraa execucao. O tamanho do codigo e conhecido na altura da compilacao. Supoe-se queexiste um controlo sequencial da execucao dum programa. Isto e, que todas as instrucoesdum programa, excepto instrucoes de salto, sao executadas sequencialmente umas apos aoutra. Os dados sao guardados na memoria ou nos registos da maquina (que executa). Amemoria esta organizada em palavras. Esta e acedida via um endereco que e representadopor um inteiro.

Os valores simples sao guardados numa unidade de memoria, os valores complexos oucompostos (vectores, estruturas) sao guardados em celulas contıguas da memoria ou emestruturas encadeadas (uma lista podera ser representada por um elementos associado aum apontador contendo o endereco memoria do proximo elemento).

Os registos permitam o acesso rapido a dados simples. O numero de registos dependeda arquitectura alvo e e em geral relativamente pequeno.

1.2 Alocacao

Certos dados sao explicitamente manipulados pelo programa fontes pelo intermedio devariaveis. Outro sao criados pelo compilador, por exemplo para conservar os valores decalculos intermedios ou para guardar ambientes associados a valores funcionais.

Alguns dados tem uma duracao de vida conhecida na altura da compilacao. E assimpossıvel, e ate desejavel, reutilizar o espaco utilizado. A analise da duracao de vida

1

Page 2: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

de variaveis deve pelo menos ter em conta das regras de alcance (variable scope) dalinguagem fonte. De facto uma variavel que deixa de ser visıvel corresponde em geral aum espaco memoria (um dado) inacessıvel. Esta analise pode igualmente utilisar tecnicasde analise de fluxo de controlo (control flow analysis) que permitam detectar variaveisnao utilizadas. As variaveis globais do programa compilado podem ser alocadas paraenderecos fixos pelo compilador, na condicao de conhecer o tamanho de cada uma delas.

A gestao das variaveis locais, dos blocos e dos procedimentos, ou ainda do armazena-mento dos valores intermedios prestam-se bem a uma gestao de alocacao da memoriabaseada em pilhas.

Outros dados, pelo contrario, nao tem uma duracao de vida conhecida na fase decompilacao. E o caso por exemplo quando se manipula apontadores. Alguns objectosalocados nao corresponderao directamente a um valor, mas serao acessıveis pelo inter-medio do seu endereco, podendo este estar associado a uma variavel. Estes dados seraoalocados, em geral, numa outra parte da memoria, designada de heap. Para nao des-perdicar este espaco, o programador necessitara recorrer a (a) comandos que permitamuma gestao explıcita da memoria (como libertar espaco) ou (b) um programa de recuper-acao de memoria designado geralmente de GC (para Garbage Collector).

1.3 Variaveis

Uma variavel e um identificador que aparece num programa e que esta associada a umobjeto manipulado pelo programa, em geral arquivado na memoria.

Distingue-se o valor esquerdo da variavel que representa o endereco memoria a par-tir do qual e arquivada o valor do objecto, do valor direito, que representa o valor doobjecto. Algumas linguagens como o ML distiguam explicitamente os dois tipos de ob-jectos. A linguagem PASCAL e a posicao sintactica da variavel que determina se deveser interpretada como um valor esquerdo ou valor direito. A passagem de parametronas funcoes e procedimentos podem ser feitos com base no valor direito ou esquerdo dosobjecto manipulados.

Ambiente e estado. E designado por ambiente a associacao de nomes a enderecosmemoria. Esta ligacao pode ser representada por um conjunto finito de pares formadospor um identificador e por um endereco. Um ambiente define uma funcao parcial queassocia um endereco a um identificador. Designa-se por estado uma funcao parcial dosenderecos memoria para valores.

Quando se atribui um valor a uma variavel so o estado se altera. Quando se invocauma funcao ou a um procedimento possuindo parametros, o ambiente muda. A variavelintroduzida corresponde a uma nocao ligacao entre um nome e um endereco.

1.4 Semantica

Para construir uma traducao correcta e essencial conhecer as regras de avaliacao dalinguagem fonte, ou seja a sua semantica. Estas regras sao frequentemente apresentadasem termos de transformadores de estado e/ou de ambiente.

2

Page 3: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

1.5 Procedimentos e funcoes

Um procedimento tem um nome, parametros, contem declaracoes de variaveis ou deprocedimentos locais e possui um corpo. Uma funcao e um procedimento que retorna umvalor.

Os parametros formais sao variaveis locais ao procedimento que serao instanciadospelos parametros efectivos quando o procedimento for invocado. O procedimento podedeclarar variaveis locais que serao inicializadas no corpo do procedimento. O espacomemoria alocado para os parametros pode em geral ser libertado quando a execucao saido procedimento. No entanto nao se pode, em geral, controlar o numero de vezes que umprocedimento sera invocado nem quando. Por isso nao se pode em geral estabelecer estati-camente o endereco das variaveis contidas num procedimento. Estes enderecos poderaoser calculadas relativamente ao estado da pilha de operandas na altura da invocacao.

Por exemplo:

program main

var j : int;

procedure p(i,j:int);

{print(i,j);if i+j>0 then j:=j-1; p(i-2,j); print(i,j);p(j,i)}

{read(j); p(j,j); print(j)}

O numero de execucao de p depende do valor lido em entrada. Em cada entrada noprocedimento p, duas novas variaveis i e j sao alocadas. No esquema seguinte representa-se a execucao deste programa tendo em conta a leitura de o valor 1. Os objectos da pilhasao representados apos cada instrucao que modifica o estado. A pilha cresce aqui decima para baixo, os valores correntes de i e j sao separados. A execucao realiza-se daesquerda para a direita e de baixo para cima. As pilhas sao numeradas seguindo ordemda execucao (ver figura 1).

1.6 Organizacao do Ambiente

A organizacao do ambiente de execucao depende do que a linguagem autorisa:

• procedimentos recursivos

• processamento de variaveis locais

• referencias para nomes locais

• modos de passagem de parametros

• existencia de dados de tamanhos variaveis

• capacidade em devolver procedimentos ou aceitar procedimentos como parametros

• alocacao/desalocacao dinamica de dados pelo programador

3

Page 4: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

Figura 1: Chamadas a funcoes e procedimento, um exemplo.

4

Page 5: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

2 Tabelas de Activacao

Um dos aspectos importantes na organizacao da memoria e a gestao das chamadas aosprocedimentos (ou funcoes). Estas sao efectuadas pelo intermedio duma tabela de ac-tivacao (frame ou activation records em ingles). Esta tabela e um pedaco de memoriaque esta atribuida a chamada do procedimento e devolvida a saıda deste. A tabela con-tem toda a informacao necessaria a execucao do programa e arquiva todos os dados quedeverao ser restabelecidos a saıda do procedimento.

Para facilitar a comunicacao entre os diferentes tipos de codigos compilados a partirde linguagens fontes diferentes (por exemplo para poder chamar procedimentos de baixonıvel a partir de linguagens de alto nıvel), nao e raro que seja recomendado a existenciadum formato particular de tabela de activacao para uma dada arquitectura.

2.1 Dados por conservar

Quando um procedimento e invocado, o fluxo de controlo do codigo e modificado. Nocaso de o retorno normal do procedimento (a execucao do procedimento nao provocoulevantamento de excepcao, nenhuma instrucao de salto (de tipo goto) levou a execucaoa sair do procedimento), a execucao continua na instrucao que segue a chamada ao pro-cedimento. O program counter na altura da chamada deve entao ser arquivado de formaa que a execucao possa continuar normalmente apos o retorno.

Sabe-se que os dados locais ao procedimento organizam-se na pilha de operandos apartir de um endereco que e determinada na execucao e em geral salvaguardado numregisto. Quando um novo procedimento e chamado, este valor muda. E assim igual-mente necessario guardar este valor que podera ser restaurado ao fim da execucao doprocedimento invocado.

2.2 Passagem de parametros

Os parametros formais dum procedimento sao variaveis que sao inicializadas aquando dachamada ao procedimento. Existem varias maneiras de efectuar esta inicializacao.

Imaginemos o procedimento p com um parametro formal x que e invocado com oparametro efectivo e. Vamos explorar aqui as diferentes formas de efectuar esta passagem.

Passagem por valor: Neste tipo de passagem, x e uma nova variavel alocada local-mente pelo procedimento cujo valor e o resultado da avaliacao de e. Apos o fimdo procedimento, as modificacoes a que x foi submetida deixam de ser visıveis. Naausencia de apontadores, as unicas variaveis modificadas sao as variaveis nao locaisao procedimento em questao e que constam explicitamente nas instrucoes deste ul-timo. E necessario reservar um espaco proporcional ao tamanho dos parametros oque pode ser custoso no caso dos vectores.

Passagem por referencia: Calcula-se o valor esquerdo da expressao e (se e nao temvalor esquerdo, entao cria-se uma variavel que se inicializa com o valor direito dee e utiliza-se o valor esquerdo desta variavel). O procedimento aloca uma variavelx que e inicializada pelo valor esquerdo de e. Qualquer referencia a x no corpodo procedimento e interpretada como uma operacao sobre o objecto situado no

5

Page 6: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

endereco guardado em x. Este tipo de passagem ocupa um espaco independente dotamanho do parametro.

Passagem por nome: Trata-se duma substituicao textual dentro do corpo do procedi-mento dos parametros formais pelos parametros efectivos. Este tipo de passagem,baseado num mecanismo de macro, pode provocar problemas de captura de var-iaveis.

Exemplo:

swap(x,y:int)

var z:int

{z:=x; x:=y; y:=z}

Se z e t sao variaveis globais do programa, swap(z,t) nao tera o comportamentoesperado.

Pode-se renomear as variaveis locais por forma a nao interagir com as variaveis queaparecem nos argumentos. De facto o nome da variavel local z, desde que sejadiferente de x e de y, nao tem influencia sobre o codigo do procedimento.

Mas isto nao chega, por exemplo a passagem por nome de swap(i,a[i]) nao re-torna o resultado esperado. Este metodo e no entanto util para a compilacao deprocedimentos de pequeno tamanho onde o custo da gestao de chamadas a proced-imentos e importante.

Passagem por Copy-Restore Durante a chamada ao procedimento, o valor direito dee serve para inicializar a variavel x. A saıda do procedimento, o valor direito de xserve para a actualizacao do valor esquerdo de e.

Este metodo pode ter, em casos particulares, comportamentos diferentes do pas-sagem por referencia, como o ilustra o exemplo seguinte:

program main;

var a : integer;

procedure p(x:integer);

{x:=2; a:=0}

{a:=1;p(a);write(a)}

Avaliacao pregicosa (lazy evaluation - call by need) Nas linguagens funcionais, distingue-se as linguagens estritas das linguagens ditas preguicosas. Numa linguagem estritaos valores dos argumentos sao avaliados antes de serem passadas em paramet-ros duma funcao. E o caso de linguagens como OCaml e SML. Nas linguagenspreguicosas, a expressao passada em parametro a funcao f so sera avaliada se frealmente precisa do valor da expressao (origem do nome call by need).

Imaginemos que a funcao f(x) = t esteja declarada e que se pretende calcular f(e).Desde que se precise do valor de x entao o calculo de e e realizado. Apesar det utilizar varias vezes x, e importante que o calculo de e se faca uma unica vez.A expressao e e passada em parametro com o seu ambiente (estrutura arquivando

6

Page 7: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

todas as variaveis utilizadas por e), o que evita os fenomenos de captura de variaveis.Este mecanismo e assim diferente do mecanismo de substituicao textual utilizado noesquema de passagem de parametro anteriormente descrito. A linguagem Haskellutiliza a avaliacao preguicosa. Este tem a vantagem de so realizar os calculos uteis.No entanto, a necessidade de passar ambientes associados aos parametros induz umcusto. Mais, e difıcil neste tipo de linguagem de prever o momento onde efeitoslateral eventuais presentes nas expressoes passadas terao lugar (o que induz varioscomportamentos possıveis para os programas, o que e indesejavel). Por isso Haskelle uma linguagem funcional pura sem efeitos laterais, o que nao impede a linguagemde ser eficaz e de utilizacao a escala industrial.

2.3 Acesso as variaveis locais

Um procedimento pode aceder as suas variaveis locais que se encontram na sua tabelade activacao, e as variaveis globais que se encontram num endereco conhecido na alturada compilacao. Nas linguagens funcionais ou de tipo Pascal, os procedimentos podemser aninhados e o corpo dum procedimento p pode aceder a variaveis declaradas numprocedimento q que englobe p. E assim necessario aceder a tabela de activacao de q queadequadamente se encontra na pilha de chamadas por baixo da tabela de activacao de p.Infelizmente nao e possıvel determinar estaticamente (sem executar) a posicao da tabelade activacao de q em relacao a de p. E assim necessaria a utilizacao de mecanismos deencadeamentos de tabelas de activacao para encontrar a variavel adequada.

Arvore de nıvel das declaracoes Supoe-se que se tem uma linguagem funcionalou imperativo no qual os procedimentos, funcoes e variaveis podem ser arbitrariamenteaninhados. O porte das variaveis (variable scope) pode ser calculada de forma estaticaseguindo as regras habituais de visibilidade. O nıvel duma declaracao (procedimentos,funcoes ou variaveis) e o numero de procedimentos ou funcoes debaixo dos quais esta edefinida. O programa principal tem um nıvel 0, as variaveis globais definidas no programaprincipal terao o nıvel 1. Por exemplo:

program main

var t:int

procedure p(x,y:int);

var z : int

procedure q(i:int)

var u : int

begin u:=z+t+i;

if x=u or y=u then x

else if u < x then p(u,x)

else if y < u then p(y,u)

else q(i+1)

end

begin read(z); q(z) end

begin read(t);p(t,t) end

7

Page 8: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

Podemos calcular de forma estatica os nıveis de cada identificador. Introduz-se umcerto numero de definicao ligadas as declaracoes, um identificador designa indiferente-mente um procedimento, uma funcao ou uma variavel: dir-se-a que p e o pai dum iden-tificador de y se y e declarada dentro de p. Dir-se-a que p e anciao de y se p e y ou paidum anciao de y. Dir-se-a de dois identificadores que sao irmaos se tem o mesmo pai. Seo corpo dum procedimento p faz referencia a um identificador entao as regras de portesao tais que este e (a) ou uma declaracao local e p (b) ou um anciao de p (por exemploo proprio p) (c) ou um irmao dum anciao de p.

Arvore de Activacao Interessamo-nos agora pelas execucoes possıveis dum programa.Para tal introduzimos a nocao de arvore de activacao.

Os nodos desta arvore representam as chamadas aos procedimentos p(e1, · · · , en). Umnodo tem k filhos q1 · · · qk se a execucao do procedimento em causa invoca directamenteos procedimentos q1 · · · qk (este procedimentos podem eles proprios chamar outros pro-cedimentos...). A raız da arvore e a execucao do programa principal.

Exemplo: Dar as arvores de activacao do programa anterior no caso das variaveis lidasvalerem sucessivamente 1, 1 e 0.

Durante uma execucao dir-se-a dum procedimento que este e activo se a execucao seencontra entre o inıcio e o fim do corpo do dito procedimento.

Nota-se assim que a ordem de chamada nao corresponde necessariamente a ordem dasdeclaracoes. No entanto demonstra-se que quando um procedimento esta activo entaotodos os seus ancioes sao igualmente activos (demonstracao por inducao sobre a arvorede activacao). A execucao do programa principal define a raız da arvore e este e anciaodele proprio. Seja p um procedimento activo, esta foi activada por um procedimento q naarvore de activacao cujo todos os ancioes sao activos (hipotese de inducao). Agora, porrazoes de visibilidade, sabemos que ou q e o pai de p ou ou p e o irmao dum anciao de q.Nos dois casos todos os ancioes de p estao activos.

Todos os procedimentos activos tem as suas tabelas de activacao reservadas em memoriacom todas as suas variaveis locais. Se um procedimento activa se refere a uma variavel y,esta foi declarada localmente por um dos seus ancioes (potencialmente por ele proprio).O grau de parentesco deste anciao e conhecido na altura da compilacao. Encadeandocada tabela de activacao dum procedimento p com a tabela de activacao do seu pai,encontra-se facilmente seguindo o numero de vezes adequadas de indireccoes o local ondea variavel se encontra arquivada.

Exemplo: No exemplo anterior o programa pode chamar p mas nao q. p pode invocarq mas q nao pode invocar p.

2.4 Organizacao da tabela de activacao

Se, por suposicao, o endereco do bloco de activacao local e dado por um registo fp eo contador de instrucao (program counter) e designado por pc entao uma organizacaopossıvel da tabela de activacao pode ser a seguinte:

8

Page 9: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

valores intermedios...

var. loc. k fp + k − 1... } Variaveis locais

fp → var. loc. 1 fp + 0fp do procedimento pai fp-1

fp do procedimento invocador fp-2pc na altura da chamada fp− 3

param n fp− 4... } Argumentos

param 1 fp− n− 3val. retorno fp− n− 4

...

De facto, certos valores como o valor de retorno (no caso de funcoes), ou ate mesmoos parametros, sao frequentemente arquivados nos registos.

Passagem de parametros de tamanho variavel Algumas linguagens, como o Pas-cal, autorizam a passagem por valor d’argumentos de tipo vector cujo tamanho e igual-mente um parametro. O tamanho nao e entao conhecido na altura da compilacao. Paracompilar o corpo do procedimento, decide-se de arquivar num local particular o enderecodo vector. Os dados de tamanho variavel podem entao ser alocados no topo da pilha.

Invocador-Invocado As operacoes por efectuar durante a invocacao dum procedi-mento estao partilhados entre o procedimento que invoca e o procedimento invocado.O codigo gerado pelo invocador deve ser escrito para cada invocacao enquanto o codigoescrito no invocado so aparece uma so vez.

O invocador efectua a reserva para o valor de retorno no caso das funcoes e avaliaos parametros efectivos do procedimento. Tambem pode se encarregar de salvaguardaro valor dos registos correntes (program counter, endereco do bloco de activacao) e decalcular o endereco do bloco de activacao do pai do procedimento invocador.

O invocado inicializa os seus dados locais e comeca a execucao. Na altura do retorno,o invocado pode eventualmente colocar o resultado da avaliacao num local reservado peloinvocador e restaura os registos.

3 Codigo Intermedio

3.1 Introducao

Utiliza-se frequentemente um codigo intermedio independente da maquina/arquitecturaalvo. Isto permite factorizar grande parte da tarefa de compilacao e de tornar o compi-lador mais facilmente adaptavel.

A escolha duma linguagem intermedia e muito importante. Este deve ser suficiente-mente rico para permitir uma codificacao confortavel das operacoes da linguagem fonte

9

Page 10: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

sem criar demasiadas longas sequencias de codigo. Este deve ser igualmente relativamentelimitado para que a implementacao final nao seja demasiada custosa.

Vamos descrever agora as operacoes duma maquina com pilhas (stack based machine)particular e explicaremos a seguir como gerar o codigo associado a construcoes particularesda linguagens em questao.

Notacao: Descreveremos a linguagem a partir de regras gramaticais. Especificare-mos uma funcao code que aceita como parametro uma arvore de sintaxe abstracta dalinguagem e devolve uma sequencia de instrucoes de codigo intermedio (da maquina depilhas).

Para clarificar a apresentacao utilizaremos notacoes em sintaxe concreta para repre-sentar a sintaxe abstracta.

Por exemplo se E ::= E1 +E2 e uma regra gramatical, escreveremos code(E1+E2) =· · · code(E1) · · · code(E2) para especificar o valor da funcao code sobre a arvore de sintaxeabstracta que corresponde a soma de duas expressoes.

Se C1 e C2 representam duas sequencias de instrucoes entao C1|C2 representam asequencia de instrucao obtida concatenando C2 no fim de C1. A lista vazia de instrucoese representada por [].

3.2 Princıpios de base duma maquina de pilhas particular

Dispomos duma maquina que contem uma zona de codigo C, um registo pc (programcounter) que contem o endereco da proxima instrucao por executar. as instrucoes temum nome seguido eventualmente de um ou dois argumentos. Estes argumentos serao emgeral inteiros ou etiquetas (label) simbolicas indicando as instrucoes do codigo que seraotraduzidas em inteiros numa fase de pre-analise.

A maquina contem uma pilha P que permite arquivar valores, um registo sp (stackpointer) que aponta para para a primeira celula livre da pilha. Um outro registo gp(global pointer) aponta para para a base da pilha (local onde serao arquivados as var-iaveis globais). Esta pilha pode conter inteiros, flutuantes, ou enderecos. Por convencaoguardaremos dados de grande tamanho como as strings ou vectores num espaco suple-mentar. Neste caso a pilha so arquivara o endereco destes dados no referido espaco.

Apresentamos, para cada instrucao da maquina, as modificacoes por considerar noestado da pilha e dos diferentes registos. Por exemplo:

Codigo Pilha sp pc CondicaoPUSHI n P [sp] := n sp + 1 pc + 1 n inteiro

Significa que a linguagem intermedia possui um comando PUSHI que espera um argu-mento inteiro n. Se a execucao deste comando tem lugar num estado em que a pilha valeP , em que o registo que aponta para o topo da pilha vale sp e o contador de instrucaovale pc entao a execucao do referido comando resulta num estado em que o endereco spda pilha tem o valor n e os contadores sp e pc foram incrementados de um. Esta instrucaoso se executa correctamente se a condicoes anunciadas sao verificadas. No caso contrarioverifica-se o despoletar de um erro de execucao.

10

Page 11: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

3.3 Expressoes aritmeticas

A maquina so permite a execucao de uma operacao aritmetica sobre os dois valorespresentes no topo da pilha. Estes dois valores sao, durante a execucao, retiradas da pilhae o resultado da operacao e colocado no topo da pilha.Codigo Pilha sp pc CondicaoADD P [sp− 2] := P [sp− 2] + P [sp− 1] sp− 1 pc + 1 P [sp− 2], P [sp− 1]

inteirosSUB P [sp− 2] := P [sp− 2]− P [sp− 1] sp− 1 pc + 1 P [sp− 2], P [sp− 1]

inteirosMUL P [sp− 2] := P [sp− 2]× P [sp− 1] sp− 1 pc + 1 P [sp− 2], P [sp− 1]

inteirosDIV P [sp− 2] := P [sp− 2]/P [sp− 1] sp− 1 pc + 1 P [sp− 2], P [sp− 1]

inteiros, se divisornulo entao erro

De forma similar existe um conjunto de instrucoes dedicadas a aritmetica de flutu-antes: FADD, FSUB, FMUL, FDIV e PUSHF.

Comparacoes O valores booleanos para o resultado de comparacoes podem ser repre-sentados por inteiros. O inteiro 1 representara o valor verdade e 0 o falso. A igualdadecorrespondera a mesma instrucao, quer se fale de comparar inteiros, flutuantes ou en-derecos.Codigo Pilha sp pc CondicaoINF P [sp− 2] := (P [sp− 2] < P [sp− 1])?1 : 0 sp− 1 pc + 1 P [sp− 2], P [sp− 1]

inteirosINFEQ P [sp− 2] := (P [sp− 2] ≤ P [sp− 1])?1 : 0 sp− 1 pc + 1 P [sp− 2], P [sp− 1]

inteirosSUP P [sp− 2] := (P [sp− 2] > P [sp− 1])?1 : 0) sp− 1 pc + 1 P [sp− 2], P [sp− 1]

inteirosSUPEQ (P [sp− 2] := (P [sp− 2] ≥ P [sp− 1])?1 : 0) sp− 1 pc + 1 P [sp− 2], P [sp− 1]

inteirosEQUAL P [sp− 2] := (P [sp− 2] = P [sp− 1])?1 : 0) sp− 1 pc + 1 P [sp − 2], P [sp −

1] simultaneamenteinteiros, flutuantes,enderecos

De forma similar a maquina disponibiliza um conjunto de instrucoes para flutuantes:FINF, FINFEQ, FSUP, FSUPEQ.

Geracao de codigo A representacao de expressoes aritmeticas sob a forma binariapermite gerar de forma simples codigo permitindo o calculo de expressoes aritmeticas.

O invariante por preservar descreve-se da seguinte forma: apos a execucao de code(E)(E expressao aritmetica) a partir dum estado onde sp = n, os valores de P [m] ondem < n nao foram modificados, o valor da expressao E e calculada e encontra-se em P [n]e sp = n + 1.

Se supomos que as expressoes aritmeticas sao geradas pela gramatica:

11

Page 12: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

E ::= inteiro|E1 + E2|E1 − E2|E1 × E2|E1/E2

O codigo correspondente e dado por:

code(inteiro) = PUSHI inteirocode(E1 + E2) = code(E1)|code(E2)|ADDcode(E1 − E2) = code(E1)|code(E2)|SUBcode(E1 × E2) = code(E1)|code(E2)|MULcode(E1/E2) = code(E1)|code(E2)|DIV

3.4 Variaveis

Variaveis globais Se a linguagem fonte permite a memorisacao determinados valoresem variaveis entao sera necessario recorrer mecanismos que permitam, na maquina, oarquivo e o acesso a valores. Estes mecanismso assentam em duas instrucoes que permitam(a) a atribuicao dum valor calculado na pilha para um espaco reservado para as variaveisglobais; (b) colocar no topo da pilha de operandos um valor contida numa variavel global.

Se a e um endereco na pilha e n um inteiro entao a + n designa o endereco localizadon celiulas acima de a. Estes dois comandos sao:Codigo Pilha sp pc CondicaoPUSHG n P [sp] := P [gp + n] sp + 1 pc + 1 n inteiro, gp + n < spSTOREG n P [gp + n] := P [sp− 1] sp− 1 pc + 1 n inteiro, gp + n < sp

O inteiro associado a cada variavel id atribuıdo sera calculado na altura da analisesintactica ou semantica e associada ao identificador, por exemplo, dentro da tabela desımbolos e designada por adr(id).

E preciso cuidar que os calculos intermedios nao removam os valores arquivados. Eassim importante reservar atempadamente o espaco para todas as variaveis globais antesde iniciar os primeiros calculos. O apontador de topo de pilha devera inicialmente apontarpara a primeira celula livre que esta a seguir ao espaco reservado para as variaveis globais.

Para este efeito utilizaremos uma instrucao que arquiva n valores nulos (iguais a 0)na pilha. Introduz-se igualmente a instrucao dual que permite fazer recuar o apontadorpara a pilha de n espacos para baixo, o que equivale de facto em apagar n valores dapilha.Codigo Pilha sp pc CondicaoPUSHN n P [sp + i] := 0,∀i.sp ≤ i < sp + n sp + n pc + 1 n inteiroPOPN n sp− n pc + 1 n inteiro

Admitimos agora que a nossa gramatica de expressoes e enriquecida da seguinte formapara poder acceder as variaveis globais:

E ::= id

O codigo correspondente e definido por:

code(id) = PUSHG adr(id)

no caso de uma variavel de tamanho unitario. Senao, se representa uma dado arquiv-ado sobre k palavras, por:

12

Page 13: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

code(id) = PUSHG adr(id) PUSHG adr(id) + 1 · · · PUSHG adr(id) + (k − 1)

Consideremos agora uma linguagem cujas unicas instrucoes sao sequencias de atribuicoes.A analise semantica deve determinar para cada variavel global id um espaco (relati-

vamente a gp) designado por adr(id) onde o valor correspondente sera arquivado.Acrescentamos um sımbolo nao-terminal a gramatica para representar as instrucoes

da linguagem. Extendemos a seguir a funcao code em conformidade. Assim, temos agramatica:

I ::= εI ::= I1 A;A ::= id := E

e a geracao de codigo seguinte:

code(ε) = []code(I1 A; ) = code(I1)|code(A)code(id := E) = code(E)|STOREG adr(id)

No caso dum dado codificado sobre k palavras, utilizaremos para codificar id := E asinstruccoes:

code(E)|STOREG adr(id) + (k − 1)| · · · |STOREG adr(id)

O invariante por conservar e que o codigo associado a uma sequencia de instrucoescomeca e termina com um apontador de pilha por cima do conjunto dos valores globais.

Vectores Em geral, o numero de celulas da pilha nos quais um dado e representadodepende do tipo do dado ( inteiro, real, vector, estrutura). Supoe-se que este tamanho econhecido na fase de compilacao e e designada por tamanho(id).

Os vectores sao representados por sequencias de celulas adjacentes na pilha. Se ovector t tem ındices entre m e M , comeca no endereco a e contem valores de tamanho k,entao este ocupa um espaco de tamanho (M −m + 1)× k.

Para calcular o endereco correspondente a uma expressao t[E] e necessario calcularo valor de n de E e aceder ao endereco a + (n − m) × k. Se conhecemos o valor de mna fase de compilacao entao podemos avaliar parcialmente esta expressao pre-calculandoa−m× k e arquivar este valor em t.val. Precisar-se-a posterior e simplesmente calculart.val + n× k.

Os comandos PUSHG, STOREG nao permitam aceder a um endereco conhecida na alturada compilacao. Assim para aceder a vectores precisaremos de um acesso parametrizadopor um valor calculada durante a execucao.

Em regra geral, o endereco ao qual precisamos de aceder depende dum endereco debase a que nao e necessariamente conhecido estaticamente (caso dos vectores de tamanhovariavel) e dum salto (offset) n que e igualmente calculado durante a execucao. Exten-demos assim os comandos PUSHG, STOREG, que operavam a partir dum endereco fixo gp edum salto estatico n em argumento, em comandos LOADN e STOREN que procuram estasinformacoes dinamicamente na pilha.

13

Page 14: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

Figura 2: LOADN e STOREN

Codigo Pilha sp pc CondicaoLOADN P [sp− 1] := P [P [sp− 2] + P [sp− 1]] sp− 1 pc + 1STOREN P [P [sp− 3] + P [sp− 2]] := P [sp− 1] sp− 3 pc + 1

Podemos representar graficamente o comportamento destes dois comandos concretizandoa pilha e numerando as celulas por inteiros k, k + 1, · · ·: ver figura 2

Podemos notar que estas instrucoes poderiam ser decompostas num comando quepermita o acesso a um elemento da pilha via um endereco arquivado na pilha, e numcomando que permita realizar calculos sobre os enderecos.

Acrescentamos a nossa linguagem os vectores que supomos serem de uma so dimensao.Temos assim as duas novas producoes seguintes:

E ::= id[E]I ::= id[E1] := E2

Para gerar codigo, devemos poder colocar na pilha um endereco. Definimos assim ainstruccao PUSHGP que permite arquivar o endereco do apontador global na pilha.Codigo Pilha sp pc CondicaoPUSHGP P [sp] := gp sp + 1 pc + 1

Supondo que o endereco de base onde e arquivado cada vector e conhecido, o codigogerado e:

code(id[E]) = PUSHGP|PUSHI adr(id)|code(E)|ADD|LOADNcode(id[E1] := E2) = PUSHGP|PUSHI adr(id)|code(E1)|ADD|code(E2)|STOREN

Os vectores bi-dimensionais podem ser arquivados via linearizacao em linha ou aindaem coluna. Se cada dimensao tem por ındice minimal mi, ındice maximal Mi e portamanho ni = Mi − mi + 1, entao se o arquivo e realizado em linha entao teremos emprimeiro o vector t[1, i] seguido de t[2, i], . . .

O elemento t[i1, i2] e arquivado no endereco a + ((i1 −m1)× n2 + (i2 −m2))× kAqui tambem podemos pre-calcular parte da expressao re-escrevendo-a em:((i1 × n2 + i2)× k + a− (m1 × n2 + m2)× k

14

Page 15: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

Quando encontramos uma expressao id[E1, E2] sera necessario calcular o offset poraplicar a partir da base onde esta arquivado id para poder aceder ao elemento designado.Podemos generalizar este processo aos vectores de dimensao p.

No caso de vectores cujo tamanho nao e conhecido na altura da compilacao, nao serapossıvel realizar este processo de pre-compilacao.

Outros comandos de acesso Podemos igualmente precisar de ter acesso a um dadona pilha referenciado de forma estatica, a partir dum endereco arquivado na pilha. Talprocesso e possıvel com os comandos LOAD e STORE seguintes:Codigo Pilha sp pc CondicaoLOAD n P [sp− 1] := P [P [sp− 1] + n] sp pc + 1 n inteiroSTORE n P [P [sp− 2] + n] := P [sp− 1] sp− 2 pc + 1 n inteiro

3.5 Instruccoes condicionais

Para compilar expressoes condicionais e ciclos iremos precisar de comandos de salto nazona de instruccoes. As instruccoes serao designadas por enderecos simbolicos inseridosno codigo.Codigo Pilha sp pc CondicaoJUMP label sp labelJZ label sp− 1 (P [sp− 1] = 0)?label : pc + 1

Para compilar expressoes, e necessario adoptar uma convencao para a representacaodos boleanos. Existem diferentes opcoes, podemos representar o falso por 0 e o valorverdade por qualquer inteiro positivo ou utilizar somente o valor 1.

Vamos a seguir ver como gerar codigo para expressoes condicionais e ciclos:

I ::= if E then I endifI ::= if E then I1 else I2 endifI ::= while E do I done

Introduz-se um comando suplementar na linguagem intermedio LABEL label que intro-duz um endereco simbolico label correspondente ao numero da instrucao, sem modificaro estado da pilha. So o program counter e incrementado.

Exercıcio: Dar um sistema de atributo para a linguagem intermedia que permite aremocao das instrucoes LABEL e de substituir os enderecos simbolicos por enderecosinteiros.

code(if E then I endif) = code(E) | JZ new next | code(I) | LABEL new nextcode(if E then I1 else I2 endif) = code(E) | JZ new false | code(I1) | JUMP new next |

LABEL new false | code(I2) | LABEL new nextcode(while E do I done) = LABEL new loop | code(E) | JZ new next | code(I) |

JUMP new loop | LABEL new next

Para esta compilacao, e necessario criar de cada vez novas etiquetas new false,new next e new loop.

15

Page 16: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

Compilacao de expressoes boleanas As expressoes booleanas, digamos por exem-plo E, servem frequentemente para operacoes de controlo. Podemos assim considerarsistematicamente duas etiquetas E true e E false e compilar a expressao boleana E semcalcular o seu valor mas decidindo que o resultado da execucao de E deve se concluir empc = E true quando E e verdade, ou em pc = E false caso contrario. Definimos entaouma funcao code bool que aceita em parametro uma expressao boleana e duas etiquetase que devolve o codigo de controlo.

Comecemos por uma gramatica de expressoes booleanas:

B ::= B1 or B2

B ::= B1 and B2

B ::= not B1

B ::= verdadeB ::= falsoB ::= E1 relop E2

O codigo gerado correspondente e:

code bool(B1 or B2, ev, ef ) = code bool(B1, ev, new e)|LABEL new e|code bool(B2, ev, ef )

code bool(B1 and B2, ev, ef ) = code bool(B1, new e, ef )|LABEL new e|code bool(B2, ev, ef )

code bool(not B1, ev, ef ) = code bool(B1, ef , ev)code bool(verdade, ev, ef ) = JUMP ev

code bool(falso, ev, ef ) = JUMP ef

code bool(E1relopE2, ev, ef ) = code(E1)|code(E2)|code(relop)|JZ ef |JUMP ev

Neste codigo, new e representa uma etiqueta criada para o efeito. code(relop) repre-senta o operador relacional utilizado.

Nota: Numa linguagem que possibilita os efeitos laterais nas expressoes, calcularou nao as sub-expressoes duma expressao booleana pode resultar em comportamentosradicalmente diferentes.

Compilacao de expressoes com branching multiplo Tratam-se de expressoes detipo switch, case ou match que permitam o branching multiplo consoante os diferentesvalores duma expressao parametro. Estes valores sao geralmente de tipo numerico econhecidos na fase de compilacao.

switch E begin case V1 : S1

case V2 : S2...case Vn : Sn

end

Antes de mais devemos nos assegurar da semantica duma tal expressao. Uma seman-

16

Page 17: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

tica natural e:if E = V1 then S1

else if E = V2 then S2...else if E = Vn then Sn

Nao e, no entanto, a semantica da instrucao switch da linguagem C. Nesta linguagemexecuta-se todas as instrucoes Si; . . . ; Sn a partir do primeiro i tal que E = Vi. E apresenca duma instrucao break num Si que permite a saıda do switch.

if E = V1 then goto l1elseif E = V2 then goto l2...elseif E = Vn then goto lnl1 : S1...ln : Sn

break :

Podemos optimizar a compilacao se os valores dos vi sao todos distintos num intervalo[k + 1, k + n]. Cria-se entao no codigo uma tabela de branching que se inicia por LABELinicio e que integra sucessivamente as instrucoes JUMP l1, . . ., JUMP ln. Seguem-se entao ocodigo de cada ramo da estrutura condicional LABEL li | code(Si), seguida eventualmenteduma instruccao JUMP fim. E desta forma necessario dispor duma instrucao de saltoindexado que notaremos JUMPI. Esta instrucao aceita uma etiqueta e devolve o controloa instrucao cujo numero e o valor numerico da etiqueta mais o valor do topo da pilha.Codigo Pilha sp pc CondicaoJUMPI label sp− 1 label + P [sp− 1]

Insere-se entao o codigo de E : code(E) do qual e necessario subtrair a constantek : PUSHI k|SUB.

Testa-se se o valor obtido esta entre 1 e n e efectua-se o branching indexado correspon-dente, senao a instrucao e ignorada. Como o valor no topo da pilha deve ser utilizada paraduas comparacoes e o salto indexado, e preciso utiliza-la 3 vezes e, por isso, duplica-lodeu as vezes com a ajuda da instrucao DUP. Obtem-se o codigo seguinte:

DUP|DUP|PUSHI n|INFEQ|JZ fim|PUSHI 1|SUPEQ|JZ fim|JUMPI inicio|LABEL fim

Exercıcio: Modificar o esquema para o caso em que a intruccao switch inclua um casopor defeito.

3.6 Invocar procedimentos

A invocacao de procedimentos tem dois efeitos. O primeiro e de modificar o decorrersequencial da execucao, o segundo e de criar um espaco para os dados locais.

17

Page 18: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

Sub-rotinas Imaginemos que queremos reutilizar em varios locais a mesma porcao decodigo I. Para tal isola-se I e atribuimo-lhe uma etiqueta. Podemos entao substituirtodas as copias de I por uma instrucao de salto para I. O problema e conseguir voltar deI para o bom local (a instrucao que segue o salto para I) e proceder ao resto da execucao.

Se temos varias utilizacoes da rotina entao nao podemos colocar simplesmente umainstrucao de salto estatico. E assim necessario, antes do salto para I, arquivar o valor doapontador de programa ( o valor do registo pc, program counter) e restaura-lo no fim daexecucao de I.

Como as chamadas a sub-rotinas podem ser aninhadas, e necessario tornar possıvelo arquivo dum numero arbitrario de tais valores. Esta informacao poderia ser guardadana pilha de operandos, mas a maquina que utilizamos aqui tem uma pilha especialmentedirigida ao processamento deste tipo de informacao.

As instrucoes CALL e RETURN permitam o correcto processamento do apontador deprograma. A instrucao CALL toma em parametro um endereco no codigo. O contador deinstrucao coloca-se entao na posicao especificada sendo o seu antigo valor arquivado.

A instrucao RETURN descobre o antigo valor do apontador de programa e atribuı-lheo valor seguinte (isto e, aponta agora para a instruccao que segue).

Acrescenta-se entao a linguagem definicoes e invocacao de sub-rotinas:

D ::= proc p; begin I endI ::= call p

code(proc p; begin I end) = LABEL label p|code(I)|RETURNcode(call p) = CALL label p

label p e uma etiqueta unica associada ao pocedimento p.

Procedimentos com parametros Se o procedimento tem parametros entao o papeldeste procedimento e permitir a gestao na pilha do espaco necessario para estes paramet-ros (reserva e acesso). Como so se consegue fazer referencia a celulas da pilha a partir doendereco de base, e necessario dispor dum registo suplementar fp que sera actualizadoao inıcio da invocacao do procedimento e que permitira referenciar os valores locais. Nasaıda do procedimento este espaco devera ser liberto.

A chamada e o retorno dos procedimentos pode ser percebido como uma modificacaodo valor de pc e da atribuicao dum endereco de base para as variaveis locais.

No momento do retorno do procedimento, o valor de pc deve ser incrementado de 1 e ovalor de fp restituıda ao valor que tinha antes da invocacao, visto as chamadas poderemser aninhadas. A pilha ficara truncada de toda a parte alocada a seguir a chamada doprocedimento.

O papel dos comandos CALL e RETURN sera exactamente assegurar o correctoarquivo de fp e pc.

Instruccoes de manipulacao de dados locais A linguagem contem instrucoes paramanipular dados referenciadas a partir do apontador fp. As instrucoes PUSHGP, STOREG,PUSHG correspondem as instrucoes PUSHFP, STOREL, PUSHL que tem o mesmo comporta-mento so que sobre fp e nao sobre gp.

18

Page 19: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

Tabela de activacao No caso de uma alocacao estatica, a tabela de activacao seraconstituıdo (a) dos parametros do procedimento que serao instanciados no momento dachamada (b) do endereco da tabela de activacao do procedimento chamadora (c) do localpara as variaveis locais.

A gramatica para as declaracoes tem a forma seguinte:

Ds ::= Ds D; |εD ::= var id : T |proc id(Ds) Ds begin I end|

fun id(Ds) : T Ds E

Podemos calcular, por exemplo, por atributos, o nıvel de cada sımbolos de procedi-mentos, funcao ou variavel, que e o numero de procedimentos ou funcoes sob o qual estese encontra definido. Este valor corresponde a profundidade do sımbolo na arvore dedeclaracoes. O programa principal tem um nıvel igual a 0.

O offset associada a uma variavel e o inteiro que se deve adicionar a fp para obter oendereco de base da variavel.

Exercıcio: Definir um sistema de atributos para calcular os nıveis e offset das declaracoes.No momento duma chamada por valor de q(e1, . . . , en), temos de calcular o codigo

de e1, . . ., en que serao empilhados, como tambem temos de calcular o endereco databela de activacao do invocador. Para tal olhamos para os nıveis respectivos de q e doprocedimento p que chamou q. O dado importante aqui e o nıvel relativo n que e igualao nıvel de p menos o nıvel de q e que e sempre maior ou igual a −1 (= −1 quando q edeclarado dentro de p).

Calculo do endereco do vector de activacao pai Quando p chama q entao o calculodo endereco do pai de q faz-se pela sequencial de instrucao seguinte:

PUSHFP | LOAD − 1︸ ︷︷ ︸n+1 vezes

Podemos notar que a sequencia de instrucao PUSHFP | LOAD n e equivalente a PUSHL nQuando este registo e actualizado, podemos executar a invocacao com o comando CALL

q. O inıcio deste procedimento deve reservar o espaco para as variaveis locais seguida daexecucao do codigo. No fim da execucao do procedimento, o comando RETURN e invocada.Esta removera da pilha os valores locais mas nao os parametros. O invocador podera entaoretirar da pilha os parametros assim como o endereco do pai. Se o procedimento e umafuncao que retorna um valor, entao a localizacao deste ultimo devera ser devidamentereservado antes da chamada do procedimento. Escolhe-se normalmente a utilizacao doprimeiro espaco livre da pilha.

Calculo do acesso a uma variavel Se dentro dum procedimento p se pretende acedera uma variavel x calcula-se ainda uma diferenca n entre o nıvel np de p e nx, o nıvel de x,que e superior a −1. Conhece-se a offset dx de x. O codigo para aceder ao valor direitode x e assim parametrizado pelo nıvel np do procedimento dentro do qual se esta:

code var(x, np) = PUSHFP| LOAD − 1︸ ︷︷ ︸np−nx+1 vezes

|LOAD dx

19

Page 20: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

Passagem de parametros por valor No modo de passagem por valor teremos acompilacao de corpos de procedimentos seguinte:

code(proc id(Ds1) Ds2 begin I end) = LABEL label id | PUSHN tamanho(Ds2) |code(I) | RETURN

A invocacao faz-se pelos comandos seguintes:

LE ::= E | LE,EA ::= call id(LE)

A geracao de codigo duma sequencia de expressao e assim somente a concatenacaodos diferentes codigos.

O codigo associado a invocacao dum procedimento q de nıvel pq e parametrizada pelonıvel np do procedimento invocador:

code call(call q(LE), np) = code(LE) | PUSHFP | LOAD − 1︸ ︷︷ ︸np−nq+1 vezes

|

CALL label id | POP tamanho(LE) + 1

Exercıcio: Escrever o codigo de compilacao e de invocacao duma funcao.

Passagem de parametros por referencia No lugar de empilhar os valores das ex-pressoes, empilha-se os seus valores esquerdos (isto e enderecos). Os acessos as variaveisfazem-se entao via uma indireccao.

Passagem duma funcao em parametro Para compilar um procedimento que aceitauma funcao em parametro, sao necessarios duas informacoes: o endereco da funcao e oendereco do vector de activacao do pai do procedimento.

Exemplo

program main

procedure b(function h(n:int):inte);

begin print(h(2)) end

procedure c;

var m:int;

function f(n:int) : int; begin f:=m+n end

begin m:=0; b(f) end

begin c end

Para executar o codigo do procedimento e necessario conhecer o endereco do codigodeste procedimento assim como o endereco da tabela de activacao do procedimento.

Quando c deve executar b(f), este calcula a ligacao de activacao du pai estatico de f(aqui c) como se este o chamasse e passa-o a b que podera o utilizar na altura da invocacaode f .

20

Page 21: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

Retornar uma funcao como valor Algumas linguagens permitam devolver funcoescomo valores. No entanto se a linguagem autoriza o acesso a variaveis nao locais, oproblema que se coloca e o da persistencia dos valores assim manipulados.

Exemplo

let f(x) = let g(y) = x+y in g

let h = f(3)

let j = f(4)

h(5)+j(7)

O valor duma funcao e designado de fecho (closure) e e formada por um apontadorpara o seu codigo assim como por um ambiente que associa valores a todas as variaveisutilizadas no corpo da funcao e que nao sao locais na altura da definicao da funcao.

4 Utilizacao de registos

Os registos permitan arquivar informacoes e manipula-las de forma rapida. No entantoestes registos existam normalmente em numero pequeno. Convem assim geri-los da mel-hor forma.

Uma forma de o fazer e de introduzir para cada valor intermedio uma nova variavel ede construir um grafo de interferencia: os nodos sao as variaveis e temos um arco entrex e y se x e y nao podem ser arquivados no mesmo registo (i.e. as duas variaveis estaovivas na mesma altura).

Podemos igualmente considerar os registos como os nodos deste grafo e ligar a ex-istencia dos arcos como o facto de certos valores nao poderem ser arquivados no registoparticular.

Assim, determinar uma reparticao das variaveis sobre k registos resume-se em encon-trar uma k-coloracao do grafo. Visto este problema ser NP-Completo em geral, utiliza-seuma aproximacao. Se este grafo e vazio, entao a coloracao e evidente. Se o grafo con-templa um nodo x de grau estritamente inferior a k entao constroi-se um novo grafo G′

removendo este nodo e as arestas correspondentes. Se G′ pode ser colorido entao podemosencontrar uma cor para x, diferente de todos os seus vizinhos e acrescenta-lo. Nota-seque remover x de G pode diminuir o grau de outros nodos e tornar assim a coloracaopossıvel. Se o grafo so comporta nodos de grau superior a k entao escolhe-se e remove-seum nodo x e procura-se colorir o grafo resultante. Se for possıvel, olha-se para o numerode cores utilizadas para colorir os vizinhos de x. Se existe uma cor disponıvel entao epossıvel colorir x e este e de novo acrescentado ao grafo, senao a coloracao nao e possıvele x tem de ser arquivado na memoria. Para tal escolhe-se um espaco mx para arquivarx e introduz-se para cada utilizacao de x uma nova variavel xi. Se o valor de x e uti-lizado numa expressao, comecamos por colocar em xi o valor arquivado no endereco mx

da memoria. Se x e actualizada entao substitua-se x por xi e propaga-se esta instrucaode actualizacao de mx na memoria pelo valor de xi. A duracao de vida das variaveis xi

e pequena, estas nao interfiram com as outras variaveis.E necessario modificar o grafo de interferencia e recomecar a coloracao. O grafo de

interferencia pode igualmente ser utilizado para remover as instrucoes move entre registos

21

Page 22: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

associadas a instrucoes x := y. Podemos identificar x e y desde que x e y nao sao vivassimultaneamente. No entanto, esta identificacao tera como resultados o aumento dograu do nodo xy (resultado da identificacao entre x e y), o que podera fazer com que acolorizacao falhe. Efectuar uma deslocacao nos registos e menos custoso do que um acessoa memoria. Assim far-se-a a identificacao so em alturas em que se consegue guarantir apreservacao da colorizacao. E o caso quando o conjunto de vizinhos de xy contempla temestritamente menos de k nodos de grau inferior a k.

Exemplo (A. Appel) Considere-se o programa seguinte formado de atribuicoes sim-ples e de acessos a memoria. Indicamos na segunda coluna o conjunto de variaveis si-multaneamente vivas no ponto de programa anterior, sabendo que apos a execucao destecodigo as variaveis d, k e j sao vivas.

Instruccoes Variaveis Vivasg := mem [j+12] k, jh := k - 1 j, g, kf := g * h j, g, he := mem[j+8] j, fm := mem[j+16] j, f, eb := mem[f] m, f, ec := e+8 m, b, ed := c m, b, ck := m+4 m, b, dj := b b, d, k

d, k, j

Podemos olhar para os nodos do grafo nesta ordem: m, c, b, f , e, j, d, k, h e g. econstatamos que em cada instante cada nodo tem um grau inferior ou igual a 3. Podemosassim realizar uma colorizacao com 4 cores e obtemos por exemplo a atribuicao de coresseguintes: m = 1, c = 2, b = 34,f = 24, e = 4, j = 3, d = 4, k = 1, h = 1 e g = 2.

Se dispunhamos so de 3 registos, poderıamos recomecar a analise com os nodos naordem seguinte (os nodos em negrito sao os que tem um grau superior a 3 e que podemassim colocar problemas): b, d, j, e, f,m,k, g, c e h. Uma tentativa ed coloracao podeser: b = 1, d = 2, j = 1, e = 2, f = 3 , m,k, g, c e h nao permitam a coloracao dem. Podemos entao escolher arquivar m na memoria no endereco M . Devemos assimtransformar o programa:

22

Page 23: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

Instruccoes Variaveis Vivasg := mem [j+12] k, jh := k - 1 j, g, kf := g * h j, g, he := mem[j+8] j, fm1 := mem[j+16] j, f, emem[M] := m1 m1, f, eb := mem[f] f, ec := e+8 b, ed := c b, cm2 := mem[M] b, dk := m2 +4 m2, b, dj := b b, d, k

d, k, j

O nodo m de grau 5 transformou-se em dois nodos m1 e m2 de grau 2. Podemosexaminar de novo os nodos do novo grafo na ordem seguinte: h, c, m2, m1, f , e, b, d, k,ge j e nao permanecem mais dificuldades.

Este tipo de mecanismo de colorizacao pode igualmente ser utilizado para minimizar onumero de espacos memoria utilizados no arquivo em memoria das variaveis. O objectivosera assim a minimizacao do numero de move mesmo se isto aumente o numero de celulasmemoria utilizada.

5 Alocacao na Heap

Na codificacao que acabamos de abordar, os novos valores por calcular ao longo da exe-cucao sao alocados na pilha. Este princıpio funciona porque o que e alocado durante ainvocacao deixa de ser acessıvel no fim do procedimento.

Este princıpio nao consegue cobrir todos os casos possıveis. Por exemplo, se a lin-guagem permite retornar valores funcionais, entao vimos que o valor devolvido deve conteruma representacao do ambiente no qual a funcao se encontra definida.

Este ambiente que deve sobreviver ao fim do procedimento que a define incluı noentanto valores alocados na tabela de activacao do procedimento ”definidor”.

5.1 Representacao de Dados Estruturados

No exemplos anteriores, alocamos para cada variavel um espaco correspondente ao tamanhodo referido dado. Assim a passagem de vectores em parametros necessita que sejam alvosde copia. Seria mais eficiente manipular os objectos de tamanho importante pelo in-termedio dos seus enderecos. No caso dos vectores em OCaml, por exemplo, existe ummomento em que o vector e criado (por exemplo, explicitamente por [|1; 2; 3|] ou via ocomando Array.create) e um valor do tipo vector e simplesmente o endereco dum vec-tor. Naturalmente o vector pode ser alocado no corpo duma funcao e ser devolvido comoresultado da funcao. Neste caso e necessario que este seja alocado numa zona de dadospersistente. Alocar tais dados na zona das variaveis globais nao e de todo uma boa ideia.

23

Page 24: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

Pois a duracao de vida de tais objectos e em geral limitada, e assim inutil reservar espacomemoria durante toda a execucao do programa.

5.2 Alocacao dinamica

Trata-se de criar dinamicamente novos espacos memoria durante a execucao do programa,espacos que ficarao acessıveis pelo programa sem, no entanto, ficarem directamente ligadosa variaveis. A duracao de vida destes objectos nao e forcosamente ligada a duracao devida dos procedimentos. Reserva-se assim espacos para esses objectos preferencialmentenum local designado de heap. E possıvel que celulas memorias reservadas desta formadeixam de ser acessıveis durante a execucao do programa. Distingue-se nas linguagens osdados directos tais como os inteiros que serao alocados e manipulados na pilha de dadosdos dados cuja manipulacao se faz pelo intermedio dum endereco na memoria. Uma talmanipulacao e necessaria quando a linguagem contem funcoes polimorficas que podemmanipular dados de tamanho variavel durante a execucao.

5.3 Alocacao / Desalocacao explıcita

Linguagens como Pascal ou C oferecem primitivas para alocar ou desalocar partes dememoria (new/dispose em Pascal, malloc/free em C). Se todos os dados por alocar temo mesmo tamanho entao e possıvel criar uma lista encadeada na qual se podera procurarum novo espaco ou ao contrario liberar uma celula. Reserva-se o espaco que engloba oespaco necessario para o dado por alocar mais o espaco para o endereco do dado seguintena pilha dos espacos disponıveis. Se e preciso alocar objectos de tamanho diferente entaodeve-se usar a heap. A desalocacao dum dado nao liberta necessariamente um espacoem memoria suficiente para os dados seguintes, a heap transforma-se em gruyere. Isto e,mesmo se o espaco livre total e suficiente, este pode estar de tal forma fraccionado que aalocacao pode falhar.

A alocacao/desalocacao explıcita torna a programacao mais pesada e faz correr o risco(a) de utilizar desnecessariamente espaco para dados inacessıveis; ou (b) ao contrarioremover dados acessıveis. Os erros de comportamento e de execucao resultantes sao,alem disso, de difıcil diagnostico.

5.4 Alocacao / Desalocacao implıcita

Linguagens como o LISP, ML ou JAVA escolheram a via da alocacao dinamica. A execucaosera levada a alocar memoria e encarregar-se-a da sua recuperacao. E o mecanismo de”recuperacao de lixo”: Garbage collector.

Varios metodos para a recuperacao de memoria existam.

Contador de referencias Cada celula dinamicamente alocada incluı um contador queindica quantos referencias estao a apontar para ela. Quando se faz um new(p), a celula emquestao ve o seu contador inicializado a um. Uma atribuicao entre apontadores da format:=u onde t e uma expressao que deve se avaliar num valor esquerdo x que representaum apontador e cujo valor direito e um endereco para uma celula m. A expressao urepresenta igualmente um apontador que se avalia num valor ditreito que e um endereco

24

Page 25: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

para a celula n. A celula m que era previamente acessıvel por x deixou de o ser, logo oseu contador e decrementado de um. Por outro lado, a celula n e agora acessıvel a partirde x logo o seu contador e incrementado de 1. as celulas cujo contador e nulo podemser recuperadas (libertas). No entanto existem situacoes em que duas celulas apontamuma para a outra e ficam assim ambas os seus contadores com o valor 1 mesmo se ambasficaram inacessıveis.

Este metodo, alem desta situacao, e igualmente guloso em termos de tempo de exe-cucao.

Exemplo

type list = Nil | Cons of int * list ref

let x = Cons(7,ref Nil)

in let y = Cons(9,ref x)

in let t = Cons(10,ref y)

in let Cons(_,z)=x in z:=y;;

Mark and Sweep Trata-se aqui de partir das variaveis da pilha que sao acessıveis peloprograma e de marcar todas as celulas assim utilizadas, e isso de forma recursiva. Quandoo processo termina, uma segunda execucao permite libertar todos os espacos inacessıveis.

Esta recuperacao e custosa em termos de tempo, o que e problematico para as apli-cacoes em tempo real. Tem tambem o inconveniente de fragmentar a memoria. Por outrolado utiliza um processo recursivo para marcar a memoria que pode conduzir a um mem-ory overflow. Para evitar utilizar espaco memoria suplementar para gear as chamadasrecursivas pode-se efectuar um percurso em profundidade das ligacoes da memoria einverter as ligacoes entre celulas memorias para memorizar as zonas que restam por per-correr.

Stop and copy Para evitar os problemas de fragmentacao podemos decidir de semprealocar linearmente os dados. quando uma zona se encontra cheia, para-se e copia-setoda a parte util numa segunda zona de forma contıgua. Liberta-se a seguir toda a zonapreviamente ocupada. Este metodo tem o inconveniente de so poder efectivamente ocuparmetade do espaco reservado.

Metodos mixtos Os GCs modernos utilizam um compromisso entre estas diferentesmetodos. Por exemplo caml-light utiliza um GC (elaborado por Damien Doligez) destetipo. O espaco memoria e separado em duas zonas. a primeira e designada de velha eorganizada com a ajuda duma lista de locais acessıveis. Esta zona podera ser extendidadinamicamente se necessario. A outra zona, designada de nova esta organizada como umvector linear de tamanho fixo. As celulas sao alocadas linearmente no vector. Quando estevector se encontra cheio. Parde tamanho fixo-se o processo e copia-se todos os objectosuteis (os que sao acessıveis a partir das variaveis da pilha, dos registos, dos objectos dazona velha, estes estando referenciados numa tabela de referencias) na zona velha. Estaetapa chama-se um GC menor. Faz-se em seguida uma fase de GC maior. Esta consistenum mark and sweep incremental da zona velha. Para marcar estes objectos utiliza-seuma colorizacao. Os nodos sao brancos quando nao foram visitados, cinzentos quando

25

Page 26: Compiladores - Geração de Código Intermédiodesousa/2012-2013/LFC/geracodigo.pdfTitle Compiladores - Geração de Código Intermédio Author Simão Melo de Sousa Created Date 9/11/2006

foram visitados mas nao os seus filhos, pretos quando foram visitados e os filhos tambem.Guarda-se uma pilha de nodos cinzentos, quando nao resta nenhum nodo cinzento amarcacao/coloracao terminou.

Os nodos que ficaram com a cor branca podem ser libertos para a lista de celulaslivres da zona velha. Para gerir o GC, a representacao de CAML reserva um bit sobrecada palavra para distinguir os enderecos (que o GC deve seguir) dos inteiros. A heape organizada em zonas correspondentes as diferentes estruturas alocadas (vectores, tipode dados estruturados, fecho, etc.). O cabecalho de cada zona reserva-se dois bits para amarcacao (branco, cinzento e preto) do GC.

26