Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a...

30
Construindo Compiladores Utilizando o Microsoft Phoenix Framework Guilherme Amaral Avelino 1

Transcript of Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a...

Page 1: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

Construindo Compiladores Utilizando o Microsoft Phoenix

Framework

Guilherme Amaral Avelino

1

Page 2: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

1 Phoenix Framework

Phoenix é uma plataforma desenvolvida para a construção de compiladores e de ferramentas de

análise otimização de código. Desenvolvida pela Microsoft, utilizando C++/CLI, ele consegue obter

o melhor de dois mundos: desempenho, proporcionado pela linguagem C++ e extensibilidade, devido

a integração com o ambiente .NET.

Os compiladores atuais funcionam como caixas pretas, onde todo o processo interno é escondido

do usuário e não é permitido fazer alterações no seu funcionamento. Tudo que o usuário pode fazer é

fornecer o código fonte como entrada, passar alguma diretivas de compilação e aguardar o programa

compilado. Phoenix objetiva abrir esta caixa. Um compilador escrito utilizando Phoenix é formado

por uma lista de fases, sendo cada fase responsável por uma etapa do processo de compilação. Através

do mecanismo de plugins, Phoenix permite que alteremos o comportamento do compilador acrescen-

tando, retirando ou alterando fases. A existência de uma representação intermediária própria, bem

como uma rica API para manipulação desta, facilitam a manipulação do compilador e a construção

de ferramentas de análise e otimização.

Phoenix proporciona um rico ecossistema para três públicos diferentes:

• Acadêmicos: permite uma melhor compreensão do funcionamento de um compilador e das

técnicas empregadas em sua construção.

• Pesquisadores: fornece um ambiente maleável, extensível e redirecionável que permite fácil

incorporação e testes de novas características a serem pesquisadas, bem com a incorporação de

novas técnicas.

• Desenvolvedores: facilita a criação de ferramentas customizadas para análise do código e per-

mite a manipulação de programas já compilados.

Além de todas estas características, Phoenix é a base para a construção dos novos compiladores

(JITs, PreJITs e C++ Backend) e ferramentas da Microsoft.

1.1 Exemplos de Utilização

Phoenix é uma ferramenta bastante poderosa, dentre outras coisas podemos utilizá-la para

• Testar novas técnicas de compilação. Um pesquisador que tenha desenvolvido uma nova técnica

de compilação, por exemplo um novo algoritmo de otimização de código, necessita incorporá-

la em um compilador para poder validar sua funcionalidade. Antes do Phoenix você teria duas

2

Page 3: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

alternativas, uma seria criar seu próprio compilador, o que além de trabalhoso provavelmente

resultaria num compilador não muito usual e a outra seria tentar incorporar sua técnica em

algum compilador de código aberto existente.

Com o Phoenix, o código da nova técnica deveria ser escrito em uma nova fase e incorporado a

um plugin o qual seria responsável por integrá-lo ao compilador, inserindo o na lista de fases do

compilador. Vale ressaltar que o SDK do Phoenix vem com um compilador C/C++ completo,

o qual poderia ser utilizado, sem que necessitássemos alterar uma linha sequer de seu código.

• Criação de Add-ons para o Visual Studio. Podemos usar o Phoenix para obter informações do

código do programa e, através destas, extender as funcionalidades do Visual Studio. O RDK

do Phoenix provê dois exemplos de tal funcionalidade, um gráfico de fluxo de controle e uma

ferramenta de de slicing dinâmica.

• Instrumentação de código. Phoenix permite a alteração de código após sua compilação. Através

de um Reader é feita a transformação de um arquivo executável na representação intermediária

provida por Phoenix. Utilizando a API Phoenix, podemos manipular esta representação e após

o término da manipulação utilizamos um Writer, que fará a conversão da representação inter-

mediária gerando, novamente, o programa. Esta técnica é largamente empregada para adição de

novas características em programas já compilados, para análise dinâmica de programas e para

prover técnicas de programação orientada a aspectos.

• Criação de compiladores. Principal foco do Phoenix, a construção de compiladores é facilitada

devido ao suporte provida por sua API tanto para a manipulação da representação intermediária,

como da geração de código final para diversas arquiteturas.

Estes exemplos demonstram a larga utilização possível deste framework. Entretanto, muito mais

pode ser feito utilizando sua API, readers e writers.

2 Inicializando a Infra-estrutura de um Compilador

Nesta seção serão apresentados classes e métodos fornecidos pelo framework Phoenix que auxil-

iam na implementação de um compilador.

2.1 Inicializando o Ambiente Phoenix

A primeira tarefa a ser executada por nosso compilador é inicializar o ambiente Phoenix e setar

arquitetura e ambiente de execução alvos da compilação.

3

Page 4: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

Para trabalharmos com binários x86 devemos setar a arquitetura e o ambiente de execução da

seguinte forma:

1 Phx.Targets.Architectures.Architecture arch =

2 Phx.Targets.Architectures.X86.Architecture.New();

3 Phx.Targets.Runtimes.Runtime runtime =

4 Phx.Targets.Runtimes.Vccrt.Win32.X86.Runtime.New(arch);

5 Phx.GlobalData.RegisterTargetArchitecture(arch);

6 Phx.GlobalData.RegisterTargetRuntime(runtime);

Código 1: Setando arquitetura e ambiente de execução x86

Os dois primeiros comandos criam, respectivamente, a arquitetura e o ambiente de execução, os

quais, são posteriormente adicionados a infraestrutura de dados do Phoenix.

Seguindo o mesmo modelo, para trabalhar com binários MSIL devemos inserir o seguinte código:

1 Phx.Targets.Architectures.Architecture msilArch =

2 Phx.Targets.Architectures.Msil.Architecture.New();

3 Phx.Targets.Runtimes.Runtime winMSILRuntime =

4 Phx.Targets.Runtimes.Vccrt.Win.Msil.Runtime.New(msilArch);

5 Phx.GlobalData.RegisterTargetArchitecture(msilArch);

6 Phx.GlobalData.RegisterTargetRuntime(winMSILRuntime)

Código 2: Setando arquitetura e ambiente de execução MSIL

Após setarmos arquitetura e ambiente de execução alvos de nosso compilador devemos inicializar

a infra-estrutura do Phoenix. O método Phx.Initialize.BeginInitialization sinaliza o começo do

processo de inicialização da infra-estrutura. Após este método devemos adicionar os Controls que

alteram o funcionamento do nosso compilador e finalizar a inicialização da infra-estrutura através da

chamada ao método Phx.Initialize.EndInitialization. Esta inicialização de infra-estrutura é respon-

sável por inicializar aspectos chaves do Phoenix, tais como: gerenciamento de memória, criação da

unidade global e de sua tabela de símbolos, etc...

O método Phx.Initialize.EndInitialization recebe dois parâmetros: uma string (parserString)

contendo a forma de como deve ser feita o parse e um array de string (argv) contendo a linha de

comandos. O parseString especifica a ordem de parse dos elementos, consiste de um ou mais ítens

de inicialização separados por ". Em todos os exemplos disponibilizados no Phoenix RDK 2007 o

argumento parseString é preenchido com "PHX|*|_PHX_|", o que significa que primeiro será feito

4

Page 5: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

o parse no ambiente de variáveis PHX, depois na string de linha de comandos e então no ambiente

_PHX_.

2.2 Phases

O processo de compilação de um programa pode ser dividido em diversas fases (Phases), onde

cada fase executa uma atividade diferente sobre o programa a ser compilado. Desta forma, uma fase

pode, por exemplo, ser responsável por alocar registros enquanto outra pode gerar funções inline. É

através de uma lista de fases que são executados todas as etapas da compilação de um programa no

ambiente Phoenix.

2.2.1 Gerando uma Lista de Fases

Após a inicialização do Phoenix, devemos construir a lista de fases que compõem o compilador. Uma

lista de fases permite que as fases sejam executadas seqüencialmente seguindo a ordem que as fases

foram inseridas. Uma vez que uma lista de fases herda da classe Phase, elementos de uma lista

de fases podem conter outras listas de fases aninhadas, devendo todos as fases da lista aninhada ser

executadas antes que se execute o próximo elemento da lista.

A lista de fases deve ser inserida em um objeto Phx.Phases.PhaseConfiguration, este objeto

é um envoltório que guarda a lista de fases e possui métodos que permitem a execução da lista.

Cada PhaseConfiguration pode ter associado a ele dois eventos: Phx.Phases.PrePhaseEvent e

Phx.Phases.PostPhaseEvent que são levantados, respectivamente, antes e após a execução das fases.

Os comandos necessários para criar uma lista de fases e inserir uma fase pode ser observados no

código 3.

1 Phx.Phases.PhaseConfiguration config =

2 Phx.Phases.PhaseConfiguration.New(Phx.GlobalData.GlobalLifetime,

3 "Hello Phases");

4 Phx.Phases.PhaseList phaseList = config.PhaseList;

5 phaseList.AppendPhase(Phx.Types.TypeCheckPhase.New(config));

Código 3: Criação de uma lista de fases

Neste exemplo primeiro criamos objeto PhaseConfiguration (linhas 1 a 3) através do método New,

criamos em seguida um PhaseList que passa a apontar para a lista contida no objeto config. Através

do método AppendPhase adicionamos uma nova fase. A fase adicionada é uma fase já existente no

5

Page 6: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

Phoenix, responsável por fazer a checagem de tipos. Podemos adicionar nossa própria fase, o que

será necessário para a construção de um compilador. A próxima seção explica como criar uma fase.

O método PhaseList.DoPhaseList é o responsável por executar a lista de fases. Este método

recebe como parâmetro um objeto Phx.Unit sobre o qual todas as fases serão executadas.

2.2.2 Criando Uma Fase

Para criar uma fase, devemos construir uma classe que herde da classe abstrata Phx.Phase e imple-

mente os métodos New e Execute. Além destes dois métodos existem outros métodos que são virtuais

e, desta forma, podem ser sobrescritos, sendo suas implementações opcionais para a construção de

uma fase. No método New deve ser criada uma instância da nova fase, já o método Execute é o

responsável por realizar as operações da fase, devendo nele conter todo o código da funcionalidade

da fase.

O método Execute recebe como argumento um objeto do tipo Phx.Unit. Embora, uma fase possa

operar sobre qualquer membro da hierarquia Unit a maioria das fases opera sobre FunctionUnits.

É comum termos Controls associados a fases. Controls são objetos que encapsulam opções de

linha de comando. Desta forma é comum utilizar Controls para habilitar e desabilitar a execução de

fases. O controle da execução ou não de um fase é feita através do método ShouldExecute, o qual,

é responsável por verificar o estado do Control correspondente, caso este tenha sido explicitamente

especificado em linha de comando. Caso contrário, SouldExecute irá verificar a propriedade IsOn-

ByDefault para tomar sua decisão. Tal propriedade tem como valor default true, mas seu valor pode

ser setado como falso no método New.

Seguindo este modelo vamos criar uma fase, TestPhase. Esta fase atuará imprimindo o nome de

todas as unidades que forem submetidas a lista de fases. O código 4 mostra como fazemos isto.

No método New criamos uma nova instância do objeto TestPhase e inicializamos a fase através do

método Initialize (linha 7), o qual recebe como parâmetro um objeto PhaseConfiguration e uma

string descrevendo o nome da fase adicionada. É através desta string que é feita a busca por fases

dentro de uma lista de fases. O método Execute contém o código funcional da fase criada.

1 class TestPhase : Phx.Phases.Phase {

2

3 public static TestPhase New (Phx.Phases.PhaseConfiguration config)

4 {

5 TestPhase phase = new TestPhase;

6

Page 7: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

6

7 phase.Initialize(config, "Lista Nomes das Unidades");

8

9 // Um controle poderia ser adicionado aqui para controlar a fase

10

11 return phase;

12 }

13

14 override void Execute (Phx.Unit unit)

15 {

16 Phx.Output.WriteLine("TestPhase {0}", unit->ToString());

17 }

18 }

Código 4: Criando uma fase

Esta fase pode ser adicionada a nossa lista de fases, criada no código 3, através do comando

AppendPhase.

Duas fases essenciais de um compilador são EncodePhase e EmitPhase, responsáveis, respectiva-

mente, por gerar a representação intermediária e por emitir o nosso programa binário no final. Estas

duas fases são específicas para cada código gerado devendo ser criadas e adicionadas ao final da lista

de fases do compilador.

2.3 Plugins

Um artifício bastante poderoso provido por Phoenix é o mecanismo de plugins. Plugins são mó-

dulos externos criados através de código gerenciado e armazenado em um arquivo dll que pode ser

adicionado a programas construídos utilizando o Phoenix. Através deste mecanismo podemos mod-

ificar a lista de fases que compõe um programa Phoenix substituindo, alterando ou inserindo fases.

Esta funcionalidade permite a modificação destes programas após sua compilação sem que para isto

precisemos alterar seu código fonte.

Na figura 1 podemos observar a utilização do plugin MyPlugin.dll. Este plugin atuará modificando

o comportamento do compilador cl (compilador para código C/C++ construído utilizando o Phoenix).

O compilador cl é dividido em dois módulos, um frontend (C1.exe) e o backend (C2.exe). O C2 é

responsável pela geração de código final e foi construído utilizando o framework Phoenix. O plugin

7

Page 8: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

irá modificar a lista de fases que compõem o backend c2 alterando assim seu funcionamento, o que

poderá ser refletido no programa gerado pelo compilador (App.exe).

Figura 1: Utilizando um plugin para alterar o comportamento do compilador cl

2.3.1 Criando um Plugin

Para criarmos um plugin devemos criar uma nova classe que estenda da interface Phx.Plugin e im-

plementarmos seus métodos RegisterObjects e BuildPhases. O primeiro método é responsável por

criar e registrar possíveis controles (Phx.Controls) que atuem sobre o plugin, já o segundo é respon-

sável por percorrer a lista de fases do programa alvo e inserir a fase encapsulada pelo plugin no local

escolhido.

No código 5 construímos um plugin que insere a fase TestPhase, construída anteriormente, na

lista de fases de um programa, antes da fase Encoding. Podemos observar que o plugin não tem seu

comportamento influenciado por nenhum controle e por isto o método RegisterObjects está vazio.

Já o método BuildPhases faz a busca na lista de fases do programa e insere a fase TestPhase antes da

fase escolhida.

1 public class PlugIn : Phx.Plugin

2 {

3 public override void RegisterObjects() { }

4

5 public override void BuildPhases

6 (Phx.Phases.PhaseConfiguration config)

7 {

8 Phx.Phases.Phase encodingPhase;

9 Phx.Phases.Phase testPhase = TestPhase.New(config);

10 encodingPhase = config.PhaseList.FindByName("Encoding");

8

Page 9: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

11 encodingPhase.InsertBefore(testPhase);

12 }

13 }

Código 5: Criando um plugin

Para permitir que plugins alterem as fases que compõe nosso programa devemos habilitar tal opção.

Para isto após criarmos o objeto PhaseConfiguration, que encapsula a lista de fases do programa,

devemos chamar o método Phx.GlobalData.BuildPlugInPhases passando ele como argumento.

A instalação do RDK do Phoenix se integra automaticamente ao Visual Studio e traz consigo

alguns assistentes de criação. Dentre estes assistente existe um para a criação de plugins. Este recurso

é bastante útil pois o VS faz todo o trabalho repetitivo, como criação de fase inserção no local indicado

da lista de fases, permitindo que o programador se preocupe exclusivamente com a funcionalidade do

plugin.

3 Principais Elementos de um Compilador

Esta seção se dedica a demonstrar como utilizar o Phoenix para criar as estruturas necessárias

para que um compilador possa gerar um programa. Serão abordados nesta seção a representação

intermediária (IR), sistema de símbolos, sistema de tipos, lifetimes e hierarquia de unidades. Seções

subseqüentes demonstrarão como utilizar estas estruturas para representar componentes específicos

de um programa tais como: funções, variáveis, chamadas de funções, desvios condicionais, etc...

3.1 Representação Intermediária Phoenix (IR)

Phoenix utiliza uma representação intermediária (IR) fortemente tipada e linear para representar

o fluxo de instruções de uma função como uma série de controles de fluxo de dados. A IR permite

que uma função seja representada em diversos níveis de abstração, podendo representar uma função

desde uma forma independente de máquina, alto nível(HIR), ou mesmo, uma forma dependente da

máquina alvo, baixo nível(LIR). Existem quatro níveis de representação provido por Phoenix, são eles

HIR, MIR, LIR e EIR.

Phoenix armazena a IR de uma função como uma lista de elementos duplamente ligados. Ela é

duplamente ligada porque cada instrução possui um operador que é ligado a duas listas de operandos:

uma contendo os operadores de origem e a outra com os de destino. Esta representação mostra de

forma explícita todos os efeitos coletarais possíveis de uma instrução, uma vez que, todos os recursos

9

Page 10: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

lidos aparecem na lista de origem e todos recursos potencialmente alterados estão especificados na

lista de destino. A representação IR de uma instrução pode ser observada na figura 2.

Figura 2: HIR de um comando X = ADD X, *P

3.1.1 Tipos e Campos

Phoenix armazena tipos em operandos que referenciam dados. O tipo de de uma instrução IR é deter-

minada pelo operador e pelos tipos dos operandos de origem. Uma vez que, a IR é fortemente tipada

o framework executa uma checagem de tipos dos operandos para garantir que eles estão corretos.

Os tipos armazenados em um operando indica o tipo atual que está sendo referenciado. Em alguns

operandos, tais como VariableOperand, existe um símbolo opcional de referência. Este símbolo

pode também ter um símbolo, mas não necessariamente o mesmo do operando (casting em C/C++).

Para garantir que operandos de tipos e campos reflitam informações precisas, a IR permite que oper-

ações de casting sejam armazenadas dentro deste operandos.

Assim como tipos, referências a campos (fields) podem ser guardadas dentro de um operando. Um

campo consiste de um tipo, um offset, e um enclosing type (tipo ao qual o campo está confinado).

Campos permitem acesso a dados dentro de tipos agregados aggregate types tais como estruturas,

uniões, ou classes.

3.1.2 Operandos

Phoenix representa cada operando como um nó folha do gráfico da IR. Operandos aparecem tanto na

lista de origem quanto na de destino. Uma vez que, que todos efeitos das instruções são representa-

dos explicitamente, operandos refletem todos os potenciais recursos usados. O que inclui, registros,

alocações de memória e códigos condicionais.

A IR armazena cada única instrução de referência como uma cópia única desse operando. Desta

maneira, operandos podem ser conectados dentro de gráficos de fluxo de dados tais como Static Single

Assignment (SSA), use-definition (use-def) ou definition-use (def-use) chains, e árvores de expressões.

10

Page 11: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

O framework mantém uma cópia única e constante, ou seja, cada surgimento de um operando é único.

A tabela 1 mostra os operandos existentes na IR.

Operandos Descrição

VariableOperand Uma variável de usuário ou um operando temporária do compilador ar-

mazenada em um registro ou na memória. VariableOperands pode

especificar se uma instrução usa o endereço ou o conteúdo de uma var-

iável.

MemoryOperand Operando utilizado pela IR para referenciar recursos na memória, tais

como o conteúdo de um endereço de memória ou o próprio endereço.

ImmediateOperand Operando utilizado para referenciar constantes tais como constantes de

inteiros, float, string e simbólicas, tais como FRAMESIZE.

AliasOperand Descreve efeitos colaterais de instruções may-use ou may-define (may-

def). AliasOperand frequentemente especifica informações sobre con-

juntos de registros mortos ou variáveis reference-definition (ref-def)

para chamada de funções.

LabelOperand Representam labels gerados pelo usuário ou pela ferramenta em uma

função. É utilizado por Phoenix para descrever fluxos de controle.

FunctionOperand Representa uma referência a um símbolo de uma função ou uma

chamada direta a uma função.

Tabela 1: Unidades da Representação Intermediária de Phoenix

3.1.3 Instruções

Uma vez que já definimos como Phoenix representam os operandos trataremos, agora, da represen-

tação IR das instruções. Cada instrução na IR expressa um fluxo de dados, fluxo de controle ou uma

operação. Cada instrução contém um opcode, que específica a operação, um lista de operandos fonte

e outra de operandos destinos. Tais instruções são classificadas como sendo pseudo ou real.

Instruções Reais Representam operações que modificam dados e o fluxo de controle do programa.

As instruções reais podem ser classificadas nos seguintes tipos:

• Simple: operação aritmética ou lógica que produz um valor.

11

Page 12: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

• Complex: uma chamada direta ou indireta a um outro procedimento.

• Control flow: operações de fluxo de controle do programa tais como desvios condicionais e

incondicionais.

• Summary: instruções para retirada do fluxo principal de instruções, tal como um bloco de

assembly inline.

Estas instruções são mapeadas para uma ou mais instruções de máquinas. A tabela 2 descreve a

instruções reais da IR.

Operandos Descrição

ValueInstruction Uma operação aritmética ou lógica que produz um valor.

CallInstruction Um procedimento de invocação, direto ou indiretamente.

CompareInstruction Uma instrução de comparação que gera um código condicional

BranchInstruction Um controle de fluxo para desvios condicionais e incondicionais, bem

como para diretos e indiretos

SwitchInstruction Um controle de fluxo para um switch, um desvio com multiplas alter-

nativas.

OutlineInstruction Uma instrução para retirada do fluxo principal de instruções, tal como

um bloco de assembly inline.

Tabela 2: Instruções Phoenix

3.1.4 Pseudo Instruções

Representam instruções que não alteram diretamente o comportamento do programa. São dividadas

em:

• label criam labels definidos pelo usuário. Determina pontos do código úteis para a construção

de programas de análise.

• Pragma representam diretivas e dados fornecidos pelo usuário.

• Data criam dados estaticamente alocados. Podem armazenar qualquer seqüencia de bytes de

baixo nível.

12

Page 13: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

3.1.5 API em Dois Níveis

Para facilitar a leitura das propriedades dos operandos e instruções de uma lista de instruções, Phoenix

disponibiliza todas as propriedades das classes que derivam de Operand e Instruction ainda em suas

classes base. Com esta implementação evitamos ter de fazer o casting dos elementos da lista para

poder obter as propriedades específicas das subclasses. Por outro lado, tal abordagem tem como

custo adicional overheads para fazer chamada aos métodos virtuais das propriedades. Se este custo

for relevante para a tarefa em questão deve-se fazer o casting dos objetos. Para tal tarefa, Phoenix

disponibiliza os métodos Instruction.As... e Instruction.As... responsável por fazer o casting para a

subclasse indicada.

3.2 Hierarquia de Unidades

Para representar cada uma das estruturas que compõem um programa, Phoenix possui classes

especiais que derivam da subclasse Phx.Unit. Estas unidades podem ser aninhadas formando uma

estrutura hierárquica, onde o a unidade mais externa é GlobalUnit. A tabela 3 lista cada uma das

unidades presentes na Representação Intermediária de Phoenix. Além de outras unidades, estas

unidades armazenam fluxos de instruções e variáveis inicializadas.

O método New, responsável pela instanciação de um objeto, de cada Unit tem sua própria assi-

natura. Sendo que todos recebem um objeto Lifetime correspondente. Para mais informações sobre

os parâmetros do construtor de cada unidade consulte o manual do Phoenix[?].

3.3 Lifetimes

Lifetime é um dos componentes centrais de Phoenix, ele é muito útil na criação de tempo de

vida de variáveis e funções. Estas estruturas representam uma forma que Phoenix encontrou de ele

mesmo manipular a memória, alocando memória quando cria um Lifetime e liberando quando este

objeto é deletado. Alocação de memória em compiladores possui como característica esta diferença

de tempo de vidas, onde algumas estruturas permanece durante todo o processo de compilação, en-

quanto outras duram apenas um pedaço de tempo. Para armazenar cada um das diferentes unidades

e estruturas utilizadas por um compilador temos diferentes tipos de Lifetimes. Assim, por exem-

plo, temos GlobalLifetime para representar alocações de memória que duram todo o processo de

compilação e TmpLifetime que representam alocações temporárias de memória.

A criação de um Lifetime é feita através do método Phx.Lifetime.New. Este método recebe dois

parâmetros, o primeiro é do tipo Phx.LifetimeKind que representa o tipo de Lifetime e o segundo é

do tipo Phx.Unit que será a unidade ao qual o Lifetime estará associado.

13

Page 14: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

3.4 Sistema de Símbolos

Símbolos Phoenix são associados com entidades tais como variáveis, labels, tipos, nomes de

funções, endereços, entidades de metadados e módulos importados e exportados. Símbolos provém

uma representação estrutural dos relacionamentos entre as entidades de um programa, tais como o

relacionamento entre uma função e seus argumentos e variáveis locais. Phoenix utiliza os símbolos

para referenciar tais entidades em sua representação intermediária.

Símbolos aparecem no namespace Phx.Symbols e tem em comum a classe base Phx.Symbols.Symbol.

Phoenix disponibiliza uma grande variedade de símbolos, os quais, podem ser agrupados em:

• Símbolos básicos. LocalVariableSymbol, GlobalVariableSymbol, FunctionSymbol, ConstantSym-

bol, etc.

• Símbolos que representam aspectos de módulos no formato portable Executable (PE). Import-

Symbol, ImportModuleSymbol e ExportSymbol.

• Símbolos para elementos de metadados. AssemblySymbol, ResourceSymbol, FileSymbol, etc.

A descrição de cada um dos símbolos foge ao escopo deste trabalho, para mais informações con-

sulte o manual do Phoenix cite[].

Os símbolos são agrupados dentro de tabelas, objetos Symbols.Table. Toda tabela de símbolo é

associada a uma unidade (Unit). A estrutura hierárquica das unidades provê mecanismo, básico, de

escopo para os símbolos. Desta forma, um símbolo declarado dentro de um módulo deve aparecer

dentro da tabela de símbolos de uma ModuleUnit e estará dentro do escopo desta unidade.

Uma tabela de símbolos não possui, por si só, nenhum mecanismo de busca. Para realizar uma

busca numa tabela devemos associar a ela um objeto Symbol.Map, que permitirá fazer a busca na

tabela utilizando como chave uma das propriedades do símbolo. Toda tabela possui pelo menos um

um mapeamento do tipo LocalIdMap, o qual podemos utilizar para fazer a busca na tabela através

do LocalId, que é único para cada símbolo contido na tabela. ExternIdMap e NameMap são outros

exemplos de mapeamento permitidos por Phoenix.

3.4.1 Proxies

Um proxy é uma forma especial de símbolos que permite que um mesmo símbolo apareça em mais de

uma tabela de símbolo. Por exemplo, uma variável estática que é definida dentro de uma função usa

14

Page 15: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

um proxy para indicar que é tanto, logicamente, um membro do escopo da função como, fisicamente,

uma variável global.

Um exemplo de quando devemos utilizar um proxy é quando uma instrução em uma FunctionUnit

faz referência a uma variável global. Sabendo-se, que os operandos de uma instrução só podem

referenciar símbolos na tabela de símbolos da unidade da função, para acessar uma variável global

será necessário criar um proxy para esta variável na tabela de símbolos da função.

Para criar um proxy devemos criar um objeto da classe Phx.Symbols.NonLocalVariableSymbol,

utilizando para isto seu método New que recebe como parâmetros a tabela de símbolo onde o proxy

será inserido e o símbolo externo.

3.5 Sistema de Tipos

O sistema de tipos do Phoenix provê uma flexível e extensível base para criação de funcionalidades,

tais como: geração de código, pointer tracking, otimizações de alto nível e debug do código objeto.

Além disso, melhora a robustez de compiladores e ferramentas que usam a infra-estrutura de Phoenix,

permitindo que estes executem a checagem de tipos nos diversos níveis da representação intermediária

(HIR, MIR, LIR).

O sistema de tipos do Phoenix disponibiliza diferentes tipos e modos de construção de regras para

a checagem de tipo. Desta forma, um compilador pode criar um conjunto de tipos e provê regras

customizadas para sua checagem de tipos. O sistema de tipos pode expressar tanto tipos de alto nível

como tipos de nível de máquina.

A classe abstrata Phx.Types.Type é a classe base para todas as classes suportadas por Phoenix.

Ela contém propriedades compartilhadas por todos tipos Phoenix, tais como tamanho e campos de

informação.

Um sistema de tipos Phoenix é representado por um conjunto de tipos armazenados em um objeto

Phx.Types.Table e um conjunto de regras prescritas por um objeto Phx.Types.Check. Assim, para

criarmos um compilador ou ferramenta precisamos criar apenas uma tabela, que deve ser compartil-

hada por todo a ferramenta. Vale observar que esta tabela é particular para uma arquitetura alvo uma

vez que cada arquitetura possui seu próprio sistema de tipos. Certos tipos, tais como tipos primitivos,

são disponibilizados como propriedades da tabela, sendo criados quando Phoenix gera a instância da

tabela.

15

Page 16: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

3.5.1 PrimitiveTypes

Os tipos primitivos Phoenix são representados utilizando objetos da classe Phx.Types.PrimitiveTypes.

Esta classe pode ser utilizada para representar inteiros com e sem sinal, números de ponto flutuante

de diversos tamanhos, void, bitfields e tipos desconhecidos em tempo de compilação (unknown). O

tipo Unknown é especialmente utilizado para representar um tipo referenciado por um tipo ponteiro

que é desconhecido ou desnecessário em tempo de compilação. Uma situação em que isto ocorre é na

compilação rápida just-in-time (JIT), onde é necessário saber se um ponteiro é coletado pelo coletor

de lixo, mas não é necessário saber seu tipo.

O sistema de tipos da IR inclui tipos primitivos normalmente encontrado na maioria das linguagens

e arquiteturas, tais como tipos inteiros e de ponto flutuante.

3.5.2 PointerTypes

O tipo ponteiro é criado através da classe Phx.Types.PointerType. As duas principais propriedades

de um objeto deste tipo são: ReferentType, que indica o tipo referenciado pelo ponteiro, e Point-

erTypeKind, tipo do ponteiro. A tabela 4 apresenta os possíveis tipos de ponteiros que podem ser

criados com Phoenix.

3.5.3 AggregateTypes

Um tipo agregados é um tipo que possui membros (dados, métodos ou outros tipos). Desta forma,

classes, estruturas e interfaces são tipos que devem ser representados utilizando a classe Phx.Types.

AggregateType.

Todo objeto AggregateType define um tipo distinto, não havendo equivalência estrutural entre

tipos agregados. Para representar categorias diferentes de tipos agregados, tais como classes, structs

e interfaces, a classe AggregateTypes possui meta-propriedades que especificam as diferenças fun-

cionais entre tipos diferentes. Ou seja, a combinação de meta-propriedades é que descrevem qual tipo

esta sendo modelado.

A lista de meta-propriedades é bastante extensa e deve ser consultada no manual do Phoenix. A

tabela 5 mostra meta-propriedades de tipos agregados MSIL e C++.

16

Page 17: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

3.5.4 ArrayTypes

Um tipo array define uma coleção de variáveis, de um tipo particular, que são acessadas utilizando

índices. Normalmente esta coleção possui um tamanho específico, setado em sua instanciação. Podem

ser de dois tipos, representados pelas classes:

• Phx.Types.ManagedArrayType, representa um arrays gerenciados pelo coletor de lixo, é uma

especialização de tipos agregados.

• Phx.Types.UnmanagedArrayType, representa tipos de arrays nativos, não gerenciado pelo

coletor de lixo.

3.5.5 FunctionTypes

Um tipo função descreve as estruturas que compõem uma função. São similares a protótipos de função

em linguagens como C++. Diferentemente dos outros tipos, o tipo função não possui um tamanho

associado a ele e é composto pelos seguintes elementos:

• Uma lista de argumentos (Phx.Types.FunctionArgument).

• Uma lista de valores de retorno, também do tipo (Phx.Types.FunctionArgument).

• Uma convenção de chamada (FastCall, ClrCall, CDecl, ThisCall, StdCall ou IllegalSentinel).

Depende de como é feita a chamada a uma função na arquitetura alvo.

• Um valor que especifica se a função tem um número variável de argumentos.

Existem duas formas de construir uma função no Phoenix. A mais simples e direta é através do

método Phx.Types.Table.GetFunctionType, utilizada no código 11. Esta abordagem, entretanto,

limita a função a ter apenas um valor de retorno e no máximo quatro argumentos. A outra maneira

é utilizando a classe auxiliar Phx.Types.FunctionTypeBuilder, que permite a construção de uma

maior variedade de funções. A utilização desta ultima forma pode ser visto no código 6, que cria uma

função que referencia a função printf.

1 //int __cdecl printf(const char *, ...);

2

3 Phx.Types.FunctionTypeBuilder funcTypeBuilder =

4 Phx.Types.FunctionTypeBuilder.New(typeTable);

5

17

Page 18: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

6 funcTypeBuilder.Begin();

7

8 funcTypeBuilder.CallingConventionKind =

9 Phx.Types.CallingConventionKind.CDecl;

10

11 funcTypeBuilder.AppendReturnType(module.RegisterIntType);

12

13 Phx.Types.Type charType = typeTable.Character8Type;

14 Phx.Types.PointerType ptrToCharType =

15 typeTable.GetUnmanagedPointerType(charType);

16 funcTypeBuilder.AppendArgumentType(ptrToCharType);

17

18 Phx.Types.FunctionArgument ellipsisArg =

19 Phx.Types.FunctionArgument.New(typeTable,

20 Phx.Types.FunctionArgumentKind.Ellipsis, typeTable.UnknownType);

21 funcTypeBuilder.AppendArgumentFunctionArgument(ellipsisArg);

22

23 Phx.Types.FunctionType printfType = funcTypeBuilder.GetFunctionType();

24

25 Phx.Name printfName = Phx.Name.New(lifetime, "_printf");

26

27 printfSym = Phx.Symbols.FunctionSymbol.New(moduleSymTable, 0,

28 printfName, printfType, Phx.Symbols.Visibility.GlobalReference);

Código 6: Construindo uma função printf utilizando FunctionTypeBuilder

3.5.6 Fields

Phoenix utiliza a classe Phx.Types.Field para descrever subpartes de outros tipos, tais como membros

de tipos agregados ou padrões de bits em tipos primitivos.

Um tipo Field é composto por:

• Um offset do início do tipo a que ele faz parte.

• O seu tamanho (tamanho da subparte).

• Seu tipo (tipo da subparte).

18

Page 19: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

4 Mapeando Estruturas

Para facilitar a tradução de um programa para a Representação Intermediária serão criados Tem-

plates que representarão cada uma das estruturas básicas de uma linguagem. Os templates aqui cri-

ados tentarão ser o mais genérico possível não tendo em vista nenhuma linguagem específica. O

objetivo é dar uma idéia de como representar estruturas básicas que possam ser, posteriormente, mod-

ificadas para modelar linguagens específicas.

4.1 Escopo e Funções Auxiliares

Um compilador durante a compilação de um programa necessita armazenar o conjunto de símbolos

já declarados (variáveis e funções) que podem ser utilizados num dado momento. Este conjunto de

declarações acessíveis em um dado momento representam o contexto no qual o compilador esta tra-

balhando. Na realidade, um compilador armazena uma pilha de contextos, onde sempre que críamos

uma nova função empilhamos um novo contexto e ao sairmos desta desempilhamos o contexto. Desta

forma o conceito de contexto está intimamente ligado ao conceito de escopo. Uma forma de se

representar o contexto de um compilador e, conseqüentemente, escopo é através de uma lista de ma-

peamentos NameMap. A criação deste mapeamento, bem como, de alguns métodos auxiliares pode

ser vista no código 7

1 List<Phx.Symbols.NameMap> nameMapList =

2 new List<Phx.Symbols.NameMap>();

3

4 public void BeginScope()

5 {

6 nameMapList.Insert(0, Phx.Symbols.NameMap.New(funcSymTable, 64));

7 }

8

9 public void BeginScope(Phx.Symbols.NameMap nameMap)

10 {

11 nameMapList.Insert(0, nameMap);

12 }

13

14 public void EndScope()

15 {

16 nameMapList.RemoveAt(0);

19

Page 20: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

17 }

18

19 Phx.Symbols.Symbol LookupSymbol(string name)

20 {

21 Phx.Name symName = Phx.Name.New(funcLifetime, name);

22 foreach (Phx.Symbols.NameMap nameMap in nameMapList)

23 {

24 Phx.Symbols.Symbol s = nameMap.Lookup(symName);

25 if (s != null)

26 return s;

27 }

28 return null;

29 }

Código 7: Métodos que manipulam o escopo de um símbolo

Nas linhas 1 e 2 é criada a lista de NameMap responsável por armazenar os diversos contextos

existentes num programa. BeginScope, linhas 4-12, utilizam o método Insert para criar um novo

escopo onde serão definidos os símbolos. Este novo escopo (NameMap) é inserido no início da lista.

EndScope, linhas 14-17, remove o escopo quando este não é mais necessário. Por último, linhas

19-29, temos o método LookupSymbol responsável por buscar na lista de escopos um determinado

símbolo. Esta busca se inicia no escopo corrente e desce até o escopo global.

Outra maneira, ainda mais direta, de se representar contextos utilizando o Phoenix é através dó

método Phx.Threading.Context.GetCurrent(), que retorna um contexto através do qual podemos

obter a unidade corrente. Utilizando esta metodologia, devemos pós o fim do uso de uma unidade

retirá-la do contexto corrente através do método PopUnit() de seu contexto. A busca por símbolos

utilizando esta forma de contexto deve ser feita utilizando as próprias tabelas de símbolos da unidade

corrente e de suas unidades pais, sua implementação pode ser observado no código 8.

1 Phx.Symbols.Symbol LookupSymbol(string name)

2 {

3 Phx.Unit currentUnit = Phx.Threading.Context.GetCurrent().Unit;

4

5 Phx.Name symName = Phx.Name.New(funcLifetime, name);

6

7 while (currentUnit != null)

20

Page 21: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

8 {

9 Phx.Symbols.Symbol s = currentUnit.SymbolTable.NameMap.Lookup(

symName);

10 if (s != null)

11 return s;

12 else

13 currentUnit = currentUnit.ParentUnit;

14 }

15 return null;

16 }

Código 8: Método LookupSymbol utilizando Phx.Threading.Context

4.2 Criando Variáveis

A tarefa de criar variáveis em um programa consiste, basicamente, em fazer o mapeamento do

nome da variável a um símbolo e armazenar este mapeamento na tabela de símbolos do contexto

atual. Este contexto pode ser global, ou seja vale para todo o programa, ou ainda ser local, valendo

apenas para uma parte específica do programa (bloco, função, etc).

4.2.1 Variáveis Globais

O contexto das variáveis globais é o primeiro a ser criada e so é descartado quando o programa chega

ao fim. O código 9, descreve a criação do contexto global e a inserção de variáveis nele.

1 Phx.Symbols.GlobalVariableSymbol sym

2 = Phx.Symbols.GlobalVariableSymbol.New(

3 moduleSymTable,

4 externalIdCounter++,

5 Phx.Name.New(lifetime, name),

6 type,

7 Phx.Symbols.Visibility.GlobalDefinition

8 );

9 sym.AllocationBaseSectionSymbol = moduleSymTable.ExternIdMap.Lookup(

10 (uint)Phx.Symbols.PredefinedCxxILId.Data) as

11 Phx.Symbols.SectionSymbol;

12 sym.Alignment = Phx.Alignment.NaturalAlignment(sym.Type);

21

Page 22: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

13 Phx.Section globalSection = sym.AllocationBaseSectionSymbol.Section;

14

15 // Cria uma instrução de inicialização

16 Phx.IR.DataInstruction initializerInstr =

17 Phx.IR.DataInstruction.New(globalSection.DataUnit, 4);

18 initializerInstr.WriteInt32(0, initialValue);

19 globalSection.AppendInstruction(initializerInstr);

20 sym.Location = Phx.Symbols.DataLocation.New(initializerInstr);

Código 9: Criando variáveis globais

A primeira coisa a ser feita é criar um símbolo para representar a variável. As linhas 1 a 8 mostram

o código. O método New requer como parâmetros a tabela de símbolos onde o símbolo será inserido,

o valor da Id do variável, o nome do símbolo, seu tipo e visibilidade do símbolo. Uma variável

global deve ser armazenada na seção de dados do código. Nas linhas 9 a 11 buscamos pelo símbolo

que representa esta seção e o transformamos em um objeto Phx.Symbols.SectionSymbol para então

setarmos como propriedade do símbolo criado. Linha 12 define o alinhamento do símbolo de acordo

com o seu tipo. A linha 13 é responsável por criar uma seção para definição de variáveis, a qual é

utilizada para a construção da instrução de inicialização da variável (linhas 16 a 20).

4.2.2 Variáveis Locais

Variáveis locais tem seu escopo reduzido ao contexto onde foi criado, por exemplo uma função ou

bloco. Desta forma, seu mapeamento deve ser inserido na lista de contextos na posição correta que se

refere ao contexto em que foi criado. O código 10 demonstra como criar uma variável local.

1 Phx.Symbols.LocalVariableSymbol sym

2 = Phx.Symbols.LocalVariableSymbol.New(

3 table,

4 externID,

5 name,

6 type,

7 storageClass

8 );

9

10 sym.Alignment = Phx.Alignment.NaturalAlignment(sym.Type);

22

Page 23: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

Código 10: Criando variáveis locais

O primeiro comando, linhas 1-8, criam um símbolo que representa uma variável local. O método

New recebe como argumento a tabela onde o símbolo deverá ser armazenada (tabela da unidade

a que a variável pertence), um inteiro não sinalizado que serve de identificador, o nome e tipo da

variável e um objeto do tipo Phx.Symbol.StorageClass. O objeto StorageClass define a forma de

armazenamento da função, podendo ser: uma variável local de uma função, um parâmetro de uma

função, etc. O próprio método New se encarrega de inserir o símbolo na tabela de símbolos. Na linha

10 definimos o tipo de alinhamento do símbolo, ou seja qual o espaço que ele ocupa na memória o

que depende do tipo da variável. Caso estejamos utilizando uma lista de mapeamento para gerenciar

o contexto devemos inserir o símbolo no contexto corrente.

4.3 Construindo Funções

Função é a unidade básica de qualquer programa. Para que um programa execute é necessário que

ele possua pelo menos uma função, a função _main(). É dentro de uma função que definimos todo o

fluxo de instruções que realizam as operações que compõe um programa. Nesta seção será mostrado

como criar uma função e adicionar o fluxo de instruções que define sua funcionalidade. Diferente

do que foi mostrado na seção 3.5.5, onde criamos um tipo função utilizando FunctionBuilder para

representar uma função externa, aqui utilizaremos um método da tabela de símbolos para construir o

nosso tipo função.

1 Phx.Types.FunctionType fnType = typeTable.GetFunctionType(

2 Phx.Types.CallingConventionKind.CDecl,

3 retType,

4 paramTypes[0],

5 paramTypes[1],

6 paramTypes[2],

7 paramTypes[3]

8 );

9

10 Phx.Symbols.FunctionSymbol fnSym = Phx.Symbols.FunctionSymbol.New(

11 moduleSymTable,

12 0,

13 Phx.Name.New(lifetime, fnname),

23

Page 24: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

14 fnType,

15 Phx.Symbols.Visibility.GlobalDefinition);

16

17 // Verificar aqui se o identificador da função já existe no contexto

corrente

18

19 func = Phx.FunctionUnit.New(

20 Phx.Lifetime.New(Phx.LifetimeKind.Function, null),

21 fnSym,

22 Phx.CodeGenerationMode.Native,

23 typeTable,

24 module.Architecture,

25 module.Runtime,

26 module,

27 funcCounter++

28 );

29

30 AddParametersToFuncSymbolTable(paramList);

31

32 // Build ENTERFUNC

33

34 Phx.IR.LabelInstruction enterInstr = Phx.IR.LabelInstruction.New(

35 func,Phx.Common.Opcode.EnterFunction, fnSym);

36

37 foreach (Parser.ParamInfo param in paramList)

38 {

39 Phx.Symbols.LocalVariableSymbol sym =

40 (Phx.Symbols.LocalVariableSymbol)LookupSymbol(param.name);

41 Phx.IR.VariableOperand opnd

42 = Phx.IR.VariableOperand.New(

43 func,

44 sym.Type,

45 sym

46 );

24

Page 25: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

47 enterInstr.AppendDestination(opnd);

48 }

49 func.FirstInstruction.InsertAfter(enterInstr);

50

51 Phx.IR.LabelOperand labelOpnd = Phx.IR.LabelOperand.New(func,

52 enterInstr);

53 func.FirstInstruction.AppendLabelSource(

54 Phx.IR.LabelOperandKind.Technical, labelOpnd);

55

56 // Local para inserção das intruções da função

57

58

59 exitInstr = Phx.IR.LabelInstruction.New(func,

60 Phx.Common.Opcode.ExitFunction);

61

62 functionUnit.LastInstruction.InsertBefore(

63 exitInstruction)

Código 11: Construindo uma função

Através do método GetFunctionType da tabela de símbolos criamos o tipo que define nossa

função, linhas 1 a 8. Os argumentos para este métodos são: a convenção de chamada, o tipo de

retorno da função e os tipos de até quatro argumentos da função. Nas linhas 10 a 15 é criado o sím-

bolo que representa a função. Em seguida é criado a unidade da função, seu método New recebe um

lifetime, o símbolo da função, o modo de geração de código, a tabela de tipos, arquitetura, ambiente

de execução, o módulo ao qual a função pertence e um identificador inteiro como argumentos. A

linha 30 chama um método responsável por criar símbolos do tipo LocalVariableSymbol para cada

um dos argumentos da função. Já nas linhas 37 a 48 cada um destes símbolos é transformado em

um operando de destino para a instrução EnterInstruction definida na linha 34. Esta instrução é um

label que determina o pondo entrada e deve ser inserida após a primeira instrução da função. As lin-

has 51 a 54 criam um operando label que será passada para primeira instrução da função para indicar

onde começa o fluxo de instruções da função. Após isto é aberto o bloco para inserção das instruções

que compõem a funcionalidade da função. As ultimas linhas (59 a 63) criam e inserem no fluxo de

instruções a instrução de saída da função.

25

Page 26: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

4.4 Comandos e Expressões

O fluxo de controle e as operações realizadas em um programa são definidas através de comandos

ou cálculo de expressões. Estes podem ser traduzidos diretamente em uma instrução na representação

intermediária ou em um conjunto de instruções. As seções a seguir definem como construir alguns co-

mandos e calcular expressões utilizando o Phoenix. Estas construções devem ser inseridas na função

logo após label EnterFunction, no local indicado no código 11.

4.4.1 Operação Binária

Operações binárias são bastante utilizadas para o cálculo de expressões onde um valor é calculado

a partir da aplicação de uma operação em dois valores. A construção de operações binárias é bem

direta, como pode ser observado no código 12. Este código deve ser utilizado através de uma função

que irá ter como retorno um objeto Phx.Ir.Operand, o qual é uma referência para o valor calculado

pela operação binária e pode ser utilizado em outras instruções.

1 Phx.Opcode opcode = null;

2 switch (op)

3 {

4 case "+": opcode = Phx.Common.Opcode.Add; break;

5 case "-": opcode = Phx.Common.Opcode.Subtract; break;

6 case "*": opcode = Phx.Common.Opcode.Multiply; break;

7 case "/": opcode = Phx.Common.Opcode.Divide; break;

8 default: parser.UhOh("Unknown binop: " + op); break;

9 }

10

11 Phx.IR.Instruction instr = Phx.IR.ValueInstruction.NewBinaryExpression(

12 func, opcode, type, src1, src2);

13

14 func.LastInstruction.InsertBefore(instr);

15

16 return instr.DestinationOperand;

Código 12: Operação binária

26

Page 27: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

4.4.2 Atribuição

Representam instruções que atribuem valor a variáveis já instanciadas. A primeira coisa a se fazer é

fazer a busca do símbolo da variável e utilizá-lo para criar um operando (linhas 2 a 4). A linha cinco

cria um ImmediateOperand que representa uma constante numérica a ser adicionada a variável,

entretando poderíamos substituir este operando por qualquer outro operando, como por exemplo o

operando retornado por uma expressão binária. Nas linhas 9 a 12 críamos a instrução de atribuição

e setamos seus operandos de origem e destino. A última instrução insere a instrução no fluxo de

instruções da função.

1 // Cria os operandos de origem (expressão) e destino (variável)

2 Phx.Symbols.Symbol sym = symbolTable.LookupByName(symbolName);

3 Phx.IR.VariableOperand dst = Phx.IR.VariableOperand.New(

4 functionUnit, sym.Type, sym);

5 Phx.IR.Operand src = Phx.IR.ImmediateOperand.New(

6 functionUnit, typeTable.Int32Type, 1);

7

8 // Cria a instrução de atribuição

9 Phx.IR.Instruction instr = Phx.IR.ValueInstruction.New(

10 funcUnit, Phx.Common.Opcode.Assign);

11 instr.AppendSource(src);

12 instr.AppendDestination(dst);

13

14 // Insere a instrução de atribuição

15 functionUnit.LastInstruction.InsertBefore(instr);

Código 13: Atribuição de valor a uma variável

4.4.3 Chamadas de Funções

Toda linguagem moderna possui algum mecanismo de chamada de função. A utilização de função

evita a repetição de códigos e dá maior expressividade a uma linguagem. Uma vez que já foi demon-

strado como criar uma função, será mostrado aqui como fazer a chamada a uma função passando a

ela seus parâmetros.

Inicialmente, deve ser feita a busca pelo símbolo que representa a função. Este então será utilizado

para a criação da instrução de chamada de função, linhas 4 a 8. Todos os parâmetros da função devem

27

Page 28: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

ser adicionados como operandos de origem. Como operando de destino devemos passar o operando

que receberá o valor de retorno da função e por último adicionamos a instrução de chamada ao fluxo

de instruções.

1 Phx.Symbols.FunctionSymbol myFnSym =

2 (Phx.Symbols.FunctionSymbol)LookupSymbol(name);

3

4 Phx.IR.Instruction callInstr = Phx.IR.Instruction.NewCall(

5 func,

6 Phx.Common.Opcode.Call,

7 myFnSym

8 );

9

10 foreach (Phx.IR.Operand opnd in args)

11 {

12 callInstr.AppendSource(opnd);

13 }

14

15 callInstr.AppendDestination(dstOpnd);

16

17 func.LastInstruction.InsertBefore(callInstr);

Código 14: Chamada a uma função

28

Page 29: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

Unit Descrição

FunctionUnit Um repositório para o fluxo de instruções, tabela de símbolos, gráfico de fluxo

e inter-referência (aliasing) de informações específicas para um método ou

função. Unidade alvo da maioria das alterações proporcionadas pela lista de

fases

DataUnit Uma coleção de dados relacionados, tais como um conjunto de variáveis ini-

cializadas ou o resultado de uma codificação de uma FunctionUnit. Provê

dados necessários para processar uma unidade.

ModuleUnit É uma coleção de funções que normalmente representa um programa ou ar-

quivo fonte. Pode ter uma DataUnit.

PeModuleUnit Tipo especial de ModuleUnit que representa um arquivo PE (portable Exe-

cutable), que pode ser um executável Windows (EXE) ou uma biblioteca de

link dinâmico (DLL).

AssemblyUnit Unidade de compilação de um assembly do Framework .NET. Contém uma

lista de objetos ModuleUnit. Menor unidade de re-uso, segurança e version-

amento.

ProgramUnit Unidade de compilação correspondente a uma imagem executável, podendo

ser um arquivo EXE ou um DLL. Contém uma lista de AssemblyUnits e uma

lista de ModuleUnit. A razão para conter duas listas é que arquivos Win32

não são formados por assembly e desta forma um objeto ProgramUnit pode

conter diretamente módulos que não estão dentro de assemblies.

GlobalUnit Unidade de compilação mais externa, contém uma lista de objetos Progra-

mUnit. Criada quando inicializamos a infra-estrutura phoenix, armazena,

entre outras coisas, as tabelas de símbolos e de tipos

Tabela 3: Unidades da Representação Intermediária de Phoenix

29

Page 30: Construindo Compiladores Utilizando o Microsoft Phoenix ...gaa/phoenix/tutorialphoenix.pdfmite a manipulação de programas já compilados. Além de todas estas características, Phoenix

PointerTypeKind Descrição

ObjectPointer Ponteiro gerado que aponta para o início de um objeto, podendo ser

utilizado para referenciar um objeto por completo.

ManagedPointer Ponteiro gerenciado pelo coletor de lixo

UnmanagedPointer Ponteiro não gerenciado pelo coletor de lixo

NullPointer Representa um ponteiro null

Tabela 4: Tipos de PointerTypes

Unit Descrição

Classe Msil Phx.Types.AggregateType.IsSelfDescribing,

Phx.Types.AggregateType.IsPrimary

Estrutura Msil Phx.Types.AggregateType.IsPrimary,

Phx.Types.AggregateType.IsSealed, mas não

Phx.Types.AggregateType.IsSelfDescribing

Interface Msil Phx.Types.AggregateType.IsPureInterface

Enumeração Msil Phx.Types.AggregateType.IsPrimary,

Phx.Types.AggregateType.IsSealed, não

Phx.Types.AggregateType.IsSelfDescribing

Classe C++ Phx.Types.AggregateType.IsSelfDescribing se

tiver função virtual ou multiplas classes base, normal-

mente é não Phx.Types.AggregateType.IsPrimary

Struct C/Enumeração C++ Phx.Types.AggregateType.IsPureData

Tabela 5: Meta-propriedade de tipos agregados

30