Linguagens de Programação

Enunciado do Trabalho Prático

Departamento de Informática
Universidade de Évora

2006/2007

Versão 1.01

Objectivo

O objectivo deste trabalho é a criação de uma implementação, em C, da máquina XISC. Esta implementação deverá ler um programa XISC na sua entrada padrão (stdin), executá-lo e terminar quando o programa terminar.

A Máquina XISC

A máquina XISC (iniciais de Xtrange Instruction Set Computer) é uma máquina abstracta simples, preparada para a execução de programas escritos em linguagens estruturadas em blocos, com ligação estática de nomes (lexical scope) e que permitem a definição de funções e procedimentos dentro de funções ou procedimentos.

A memória da máquina XISC divide-se em três zonas distintas:

  1. a memória de instruções, onde são guardadas as instruções do programa a executar, e cujo formato não é especificado;
  2. a pilha de avaliação, que é usada na avaliação de expressões, para a transferência de dados e para guardar o valor devolvido por uma função; e
  3. a memória de execução, onde se encontram os registos de activação dos blocos do programa cuja execução ainda não terminou.

Além daquelas zonas da memória, a máquina XISC possui dois registos: o registo EP, que contém o environment pointer; e o registo PC, com o program counter.

Caso seja considerado útil ou necessário, uma implementação poderá acrescentar outros registos que ajudem a controlar o funcionamento da máquina (como, por exemplo, um registo RA para guardar o endereço de retorno de uma função). Estes registos não poderão conter dados manipulados pelo programa.

A Linguagem Pseudo-C

O Pseudo-C é uma variante da linguagem C que permite a definição de funções locais a uma função. Esta linguagem só permite manipular inteiros e todas as variáveis são globais no corpo da função em que são declaradas, i.e., a definição de uma função tem a forma:
        tipo nome(argumentos formais)
        {
          declarações (de variáveis ou funções)

          instruções
        }
em que as instruções não incluem qualquer declaração de variável ou função.

Outra característica do Pseudo-C é todo o código de um programa constituir a definição da função void main(), como é exemplificado pelo programa seguinte, que calcula e escreve no terminal o valor do factorial de 12:

        void main()
        {
          int fact(int n)
          {
            int i, f = 1;

            for (i = 1; i <= n; ++i)
              f = f * i;

            return f;
          }

          println(fact(12));
        }

Os programas em Pseudo-C são compilados para sequências de instruções XISC, que são, posteriormente, executadas por uma implementação da máquina abstracta. Para o programa anterior, um compilador poderia gerar o seguinte código XISC:

        main:   locals 0 0
                push_int 12
                set_arg 1
                call -1 fact    # calcula fact(12)
                put             # e imprime
                put_nl
                ret

        fact:   locals 1 2
                push_int 1
                store_var 0 2   # f = 1
                push_int 1
                store_var 0 1   # i = 1
        L1:     push_arg 0 1
                push_var 0 1
                jlt L2          # n < i
                push_var 0 2
                push_var 0 1
                mult
                store_var 0 2   # f = f * i
                push_var 0 1
                push_int 1
                add
                store_var 0 1   # i = i + 1
                jump L1
        L2:     push_var 0 2
                ret             # return f
(Nos ficheiros factorial.pc e factorial.xisc encontram-se as duas versões do programa.)

Blocos em Pseudo-C

O corpo de uma função em Pseudo-C constitui um bloco de código.

A profundidade de um bloco é dada pelo número de blocos que o envolvem no texto do programa Pseudo-C. A função main é o único bloco com profundidade 0. No exemplo acima, o outro bloco lá presente, correspondente à função fact, tem profundidade 1.

Não há qualquer restrição quanto à profundidade máxima que um bloco pode ter.

As Instruções XISC

O conjunto de instruções XISC é constituído pelas instruções descritas abaixo. Um programa XISC é uma sequência de instruções XISC, onde cada instrução pode ser identificada por uma etiqueta, no formato:
<etiqueta>: <instrução>

A instruções de um programa XISC são executadas pela ordem em que aparecem no texto do programa, a partir da primeira instrução da função main, identificada pela etiqueta main. A ordem normal de execução das instruções poderá, no entanto, ser alterada devido à execução das instruções de salto ou de alguma das instruções call e ret.

Instruções aritméticas

Estas instruções executam as operações aritméticas da máquina XISC. São instruções que não possuem argumentos, operando sobre o conteúdo da pilha de avaliação.
add
Adição.
sub
Subtracção.
mult
Multiplicação.
div
Divisão (inteira).
O funcionamento destas instruções consiste em quatro passos: 1) o 2º operando é desempilhado da pilha de avaliação; 2) o 1º operando é desempilhado da pilha de avaliação; 3) a operação é efectuada; 4) o resultado é empilhado na pilha de avaliação.

Instruções de salto

Existem duas instruções de salto condicionais e uma incondicional.
jump <etiqueta>
A instrução executada a seguir a esta instrução será a instrução identificada por <etiqueta>. A instrução jump não tem qualquer outro efeito sobre o estado da máquina.
jeq <etiqueta>
Se os dois valores nas duas posições no topo da pilha de avaliação forem iguais, a instrução executada a seguir será a identificada por <etiqueta>; senão, a próxima instrução a executar será a que se segue a jeq na ordem do programa.
Em ambos os casos, os dois valores no topo da pilha de avaliação são desempilhados.
jlt <etiqueta>
Sejam A o valor no topo da pilha de avaliação e B o valor na posição imediatamente abaixo. Se B for menor que A, a instrução executada a seguir será a identificada por <etiqueta>; senão, a próxima instrução a executar será a que se segue a jlt na ordem do programa.
Em ambos os casos, os dois valores no topo da pilha de avaliação são desempilhados.

Instruções de manipulação de inteiros

push_int <inteiro>
Esta instrução empilha o seu argumento, uma constante inteira, na pilha de avaliação.

Instruções de acesso a variáveis

As operações de leitura e de escrita do valor de uma variável são efectuadas através das instruções deste grupo. As variáveis são declaradas no início dos blocos que constituem a definição das funções e não incluem os argumentos das funções.
push_var <inteiro> <inteiro>
Sejam P0 a profundidade do bloco em que uma determinada variável foi declarada, e P1 a profundidade do bloco em que ela está a ser acedida. O 1º argumento de push_var é a diferença entre P1 e P0, ou seja, a distância entre o bloco corrente e aquele em que a variável foi declarada: se a variável é local à função corrente, esse valor é 0; se a variável foi declarada na função dentro da qual a função corrente está definida, esse valor é 1; e assim sucessivamente.
O 2º argumento de push_var é o número da variável no bloco em que foi declarada. O número de uma variável pode ser qualquer, desde que, em cada bloco, todas as variáveis tenham números distintos e consecutivos, começando em 1. Nestas condições, um número de uma variável identifica-a univocamente dentro do seu bloco.
O efeito desta instrução é empilhar, na pilha de avaliação, o valor da variável com número número do bloco cuja profundidade dista distância da do bloco corrente.
store_var <inteiro> <inteiro>
Os dois argumentos desta instrução são como na instrução anterior.
Esta instrução desempilha o valor no topo da pilha de avaliação e atribui-o à variável com número número do bloco cuja profundidade dista distância da do bloco corrente.

Instruções de acesso a argumentos

O papel das instruções XISC deste grupo é o mesmo que o das do grupo anterior, mas em relação aos argumentos das funções.

Em ambas as instruções, o 1º argumento é a distância entre a profundidade da função a que o argumento pertence e a da função que lhe acede, e o 2º é o número de ordem do argumento na lista de argumentos formais da função, cabendo ao primeiro destes o número 1.

push_arg <inteiro> <inteiro>
Empilha, na pilha de avaliação, o valor do argumento com número número da função cuja profundidade dista distância da profundidade da função corrente.
store_arg <inteiro> <inteiro>
Desempilha o valor no topo da pilha de avaliação e atribui-o ao argumento com número número da função cuja profundidade dista distância da profundidade da função corrente.

Instruções para a chamada de funções

As instruções deste grupo processam a chamada de funções, o início e o fim da sua execução, através da manipulação dos registos de activação.
set_arg <inteiro>
Esta instrução desempilha, da pilha de avaliação, o valor de um argumento da função que vai ser chamada, e põe-o numa localização em que esta lhe poderá aceder.
O argumento da instrução indica o número do argumento da função.
call <inteiro> <etiqueta>
Esta instrução efectua a chamada da função cujo nome é dado por <etiqueta>, provocando a transferência da execução para a primeira instrução dessa função. Antes de poder ser utilizada, a instrução set_arg terá de ter sido utilizada, tantas vezes quantos os argumentos da função, para que estes estejam prontos quando a chamada ocorre.
O 1º argumento de call é a distância entre a profundidade da função chamadora e a da função chamada: seja f a função em cujo corpo é chamada a função g: se g está definida nas declarações de f, a distância será -1; se g e f estão definidas dentro da mesma função, a distância será 0; se f está definida nas declarações de g, a distância será 1; e assim sucessivamente.
locals <inteiro> <inteiro>
É a primeira instrução do corpo de uma função e indica quantos argumentos a função tem (o 1º argumento da instrução) e quantas variáveis locais declara (o 2º argumento da instrução).
ret
Termina a execução do corpo de uma função, provocando o retomar da execução da função que a chamou.
Se a função devolver algum valor, este é devolvido através da pilha de avaliação, e já lá deverá ter sido colocado antes de ret ser executada.

Instruções de saída

Através das instruções deste grupo é possível, a um programa, enviar informação para o écrã.
put
Desempilha o valor no topo da pilha de avaliação e escreve-o, como um inteiro, no écrã.
put_str <string>
Escreve a <string> no écrã.
put_nl
Termina a linha corrente na saída do programa. O resultado da execução da próxima instrução de saída aparecerá na linha seguinte do écrã.

Características da Implementação

Linguagem

A implementação da máquina XISC deverá ser feita em C, recorrendo ao gerador de analisadores lexicais flex e ao gerador de analisadores sintácticos bison.

São fornecidos os seguintes ficheiros:

O conteúdo destes ficheiros pode ser alterado, excepto no que diz respeito às definições dos símbolos terminais (tokens) e à gramática, a não ser para a introdução de acções semânticas.

As regras contidas no ficheiro Makefile deverão ser tais que os comandos make e make all provocarão a construção de um executável chamado xisc.

Memória da máquina

A memória de instruções terá a estrutura considerada a mais adequada e a pilha de avaliação será uma pilha de valores inteiros. A memória de execução deverá ser encarada como uma sequência de valores não estruturados e deverá ser implementada num vector (de inteiros, de caracteres, ...).

Poderão existir outras estruturas de dados, somente para o controlo do funcionamento da máquina. Estas estruturas não poderão, no entanto, ser usadas durante a execução do programa XISC para manipular os dados com que este trabalha.

Registos de activação

Faz parte da realização do trabalho o desenho dos registos de activação.

Entrada e saída de dados

A implementação da máquina XISC lerá o programa da sua entrada padrão (stdin) e não escreverá nada no écrã, excepto o resultado das instruções de saída incluídas no programa XISC em execução, que deverá ser enviado para a saída padrão da máquina (stdout).

Compilação de código Pseudo-C

Não faz parte do trabalho fazer um compilador de Pseudo-C para código XISC.

Exemplos

A seguir, apresentam-se exemplos de programas em Pseudo-C e, para cada um, uma possível tradução para código XISC.

Relatório

O relatório a entregar deverá incluir:

Entrega e Avaliação

O trabalho será realizado por grupos de até dois elementos.

O trabalho será classificado de 0 a 20 valores.

Os elementos a entregar serão:

O código entregue deverá ser compilável e funcionar na máquina alunos.di.uevora.pt.

O relatório poderá ser entregue em formato electrónico ou impresso. Se for entregue em formato electrónico, o relatório deverá estar em formato texto, HTML ou PDF (não serão aceites relatórios em formato DOC).

Os ficheiros a entregar deverão ser incluídos num arquivo .tar.gz ou .zip. O arquivo deverá ter nome nnnnn-mmmmm.tar.gz (ou .zip), onde nnnnn e mmmmm deverão ser substituídos pelos números dos elementos do grupo. No caso dos trabalhos individuais, o nome do arquivo deverá ser nnnnn.tar.gz (ou .zip). É da responsabilidade de cada grupo certificar-se de que o arquivo entregue contém exactamente o que pretendiam entregar.

A entrega deverá ser feita, através do moodle, até às 19 horas do dia 3 de Julho de 2007. (Para evitar problemas com entregas em cima da hora limite, a entrega estará aberta até às 23:55.)

Serão avaliadas: a correcção e a eficiência da solução apresentada; a relevância e a completude do relatório; a qualidade, a documentação e a apresentação do código (cada linha não deverá ter mais de 80 caracteres).

O trabalho deverá ser realizado somente por quem o entrega. A detecção de qualquer tentativa de subverter este princípio terá como consequência, para todos os envolvidos, a impossibilidade de obter aprovação à disciplina este ano lectivo.

Os docentes da cadeira agradecem que qualquer erro detectado no enunciado do trabalho lhes seja comunicado.

Bom trabalho.