Post on 03-Dec-2018
UNIVERSIDADE REGIONAL DE BLUMENAU
CENTRO DE CIÊNCIAS EXATAS E NATURAIS
CURSO DE CIÊNCIAS DA COMPUTAÇÃO – BACHARELADO
IMPLEMENTAÇÃO DE TIPOS ABSTRATOS DE DADOS NO
AMBIENTE FURBOL
ANDRÉ LUÍS GARLINI
BLUMENAU 2008
2008/1-04
ANDRÉ LUÍS GARLINI
IMPLEMENTAÇÃO DE TIPOS ABSTRATOS DE DADOS NO
AMBIENTE FURBOL
Trabalho de Conclusão de Curso submetido à Universidade Regional de Blumenau para a obtenção dos créditos na disciplina Trabalho de Conclusão de Curso II do curso de Ciências da Computação — Bacharelado.
Prof. José Roque Voltolini da Silva - Orientador
BLUMENAU 2008
2008/1-04
IMPLEMENTAÇÃO DE TIPOS ABSTRATOS DE DADOS NO
AMBIENTE FURBOL
Por
ANDRÉ LUÍS GARLINI
Trabalho aprovado para obtenção dos créditos na disciplina de Trabalho de Conclusão de Curso II, pela banca examinadora formada por:
______________________________________________________ Presidente: Prof. José Roque Voltolini da Silva – Orientador, FURB
______________________________________________________ Membro: Prof. Mauro Marcelo Mattos, Doutor – FURB
______________________________________________________ Membro: Prof. Miguel Alexandre Wisintainer, Mestre – FURB
Blumenau, 10 de julho de 2008
Dedico este trabalho à minha família.
AGRADECIMENTOS
Agradeço o meu orientador José Roque Voltolini da Silva e a minha família. Todos
foram de extrema importância para a realização deste trabalho.
Agradeço também os meus colegas e professores do curso de BCC, especialmente o
amigo Adilson Hasckel, por todos esses anos de luta e de amizade.
Toda a nossa ciência, comparada a realidade, é primitiva e infantil – e, no entanto, é a coisa mais preciosa que temos.
Albert Einstein
RESUMO
Este trabalho apresenta a implementação de Tipos Abstratos de Dados (TADs) no ambiente de programação FURBOL. O trabalho é baseado no Trabalho de Conclusão de Curso (TCC) de Paulo Henrique da Silva (SILVA, 2002), estendendo-o para oferecer suporte a TADs definidos pelos usuários da Linguagem de Programação (LP) FURBOL. Para a especificação formal da sintaxe é utilizada a notação Backus-Naur Form (BNF) e para a semântica são utilizados esquemas de tradução.
Palavras-chave: Tipos abstratos de dados. Esquemas de tradução. Linguagens de programação.
ABSTRACT
This work presents the abstract data type´s implementation in the FURBOL´s programming environment. The work is based on (SILVA, 2002), extending it to support abstract data types defined by the FURBOL´s programming language users. For the formal syntax specification is used de BNF notation and translation schemes for specification of semantics.
Key-words: Abstract data types. Translation schemes. Programming languages.
LISTA DE ILUSTRAÇÕES
Figura 1 - Paradigmas de LPs...................................................................................................19
Quadro 1 – Exemplo de uma função em Scheme.....................................................................21
Quadro 2 - Exemplo de código em PROLOG..........................................................................21
Quadro 3 - Exemplo de um TAD definido em C++.................................................................22
Quadro 4 - Trecho de uma gramática apta a análise recursiva preditiva..................................25
Quadro 5 - Exemplo de um esquema de tradução....................................................................27
Quadro 6 - Exemplo de uma gramática de atributos ................................................................27
Quadro 7 - Algoritmo para a construção de um tradutor preditivo dirigido pela sintaxe.........28
Quadro 8 - Instruções do código de três endereços ..................................................................29
Quadro 9 - Registradores do 8086/8088...................................................................................34
Quadro 10 - Propósito dos sinalizadores do registrador FLAGS .............................................36
Quadro 11 - Modos de endereçamento do 8086/8088..............................................................37
Quadro 12 - Conjunto de instruções do 8086/8088..................................................................39
Quadro 13 - Layout de memória de um arquivo .COM............................................................40
Quadro 14 - Estrutura completa de um arquivo .COM ............................................................40
Figura 2 - Interface do FURBOL .............................................................................................41
Quadro 15 - Programa em FURBOL com unidades de processos concorrentes......................42
Quadro 16 - Exemplo de programa na linguagem implementada por Tomazelli (2004).........43
Quadro 17 - Exemplo da definição de uma classe na linguagem especificada em Leyendecker
(2005).....................................................................................................................43
Quadro 18 - Definição do programa principal e blocos ...........................................................46
Quadro 19 - Definição de declarações de variáveis .................................................................47
Quadro 20 - Definição dos tipos de dados................................................................................48
Quadro 21 - Definição de sub-rotinas e procedimentos ...........................................................49
Quadro 22 - Definição de tarefas..............................................................................................49
Quadro 23 - Definição de parâmetros formais .........................................................................50
Quadro 24 - Definição dos comandos da linguagem................................................................51
Quadro 25 - Definição dos comandos da linguagem (continuação).........................................52
Quadro 26 - Definição dos comandos de atribuição, estrutura de chamada de procedimento e
parâmetros atuais ...................................................................................................53
Quadro 27 - Definição do comando de repetição e comandos condicionais............................54
Quadro 28 - Definição dos comandos de entrada e saída.........................................................55
Quadro 29 - Definição dos comandos incremento e decremento.............................................56
Quadro 30 - Definição dos comandos de criação e exclusão de semáforos .............................56
Quadro 31 - Definição dos comandos de operação P e V em semáforos.................................57
Quadro 32 - Definição dos comandos de criação e exclusão de objetos..................................58
Quadro 33 - Definição da estrutura de controle de expressões ................................................59
Quadro 34 - Definição da estrutura de controle de expressões (continuação) .........................59
Quadro 35 - Definição da estrutura de controle de expressões (continuação) .........................60
Quadro 36 - Definição da estrutura de controle de expressões (continuação) .........................61
Quadro 37 - Definição da estrutura de controle de expressões (continuação) .........................62
Quadro 38 - Definição da estrutura de controle de expressões (continuação) .........................63
Quadro 39 - Especificação dos TADs definidos pelos usuários...............................................64
Quadro 40 - Especificação de um método................................................................................65
Quadro 41 - Especificação do acesso aos membros dos TADs................................................66
Quadro 42 - Exemplo do uso do parâmetro oculto esse ........................................................67
Quadro 43 - Exemplo de um TAD definido conforme a nova especificação da linguagem
FURBOL ...............................................................................................................68
Quadro 44 - Exemplo de um programa que usa o TAD definido no Quadro 43 .....................69
Quadro 45 - Exemplo de um programa que define e usa um TAD..........................................70
Figura 3 - Esboço da tabela de símbolos para o programa do Quadro 45................................71
Quadro 46 - Exemplo de variáveis do tipo objeto....................................................................72
Quadro 47 - Exemplo da criação de um objeto ........................................................................72
Quadro 48 - Exemplo da criação de um objeto com chamada de método construtor ..............73
Quadro 49 – Uso da função 48h do MS-DOS..........................................................................74
Quadro 50 - Trecho do procedimento criarobj ..................................................................74
Quadro 51 – Localização do parâmetro esse na pilha ...........................................................75
Quadro 52 - Exemplo de programa em FURBOL com chamada de método...........................76
Quadro 53 - Código gerado para a chamada de um método ....................................................76
Quadro 54 - Código para a chamada de um método por outro método....................................77
Quadro 55 - Definição de um TAD com quatro atributos........................................................78
Quadro 56 - Código do método constroi definido no Quadro 55 ......................................78
Quadro 57 - Trecho de programa que utiliza a palavra reservada nulo .................................79
Quadro 58 - Código gerado pelo enunciado da linha 14 do Quadro 57 ...................................79
Quadro 59 - Exemplo de programa que exclui dois objetos ....................................................80
Quadro 60 - Exemplo de código gerado para exclusão de um objeto ......................................80
Quadro 61 - Exemplo de código gerado para exclusão de um objeto com chamada de destrutor
...............................................................................................................................81
Quadro 62 - Uso da função 49h do MS-DOS...........................................................................81
Quadro 63 - Trecho do procedimento destruirobj ...........................................................81
Figura 4 - Diagrama de casos de uso........................................................................................82
Quadro 64 - Caso de uso compilar ...........................................................................................83
Figura 5 - Diagrama de classes do FURBOL...........................................................................84
Figura 6 - Diagrama de seqüência para o caso de uso compilar...............................................86
Quadro 65 - Declaração das palavras reservadas do FURBOL ...............................................88
Quadro 66 - Implementação do não-terminal TAD...................................................................89
Quadro 67 - Implementação do não-terminal MetodoTAD ....................................................91
Quadro 68 - Implementação do não-terminal Esse ................................................................93
Quadro 69 - Código do método ProcurarSimboloNivel do gerenciador de símbolos..93
Quadro 70 - Exemplo de exceção gerada pelo compilador ......................................................94
Quadro 71 – Método que gera código que atribui ao registrador de segmento ES o conteúdo
do parâmetro esse ...............................................................................................94
Quadro 72 - Método que gera código assembly para acessar os atributos de um TAD...........95
Quadro 73 – Trecho de código do compilador que invoca os métodos apresentados no Quadro
71 e no Quadro 72..................................................................................................95
Figura 7 - Acesso as principais funcionalidades do FURBOL.................................................96
Figura 8 - Ambiente exibindo o código intermediário gerado pelo compilador ......................97
Figura 9 - Ambiente exibindo o código de montagem gerado pelo compilador ......................98
Figura 10 - Ambiente exibindo a saída do montador e do ligador ...........................................99
Quadro 74 - Atalhos no teclado do ambiente ...........................................................................99
Quadro 75 – Exemplo de problema com parâmetros por referência......................................100
Quadro 76 - Exemplo de uma expressão que apresenta um resultado incorreto....................100
Quadro 77 - Limitações do trabalho com relação aos arrays.................................................101
Quadro 78 - Limitações do trabalho com relação ao uso de símbolos ainda não definidos...101
Quadro 79 - Procedimento do núcleo criarobj .................................................................106
Quadro 80 - Procedimento do núcleo destruirobj ..........................................................107
Quadro 81 - Procedimento do núcleo imprime ...................................................................108
LISTA DE SIGLAS
BCD – Binary-Coded Decimal
BNF – Backus-Naur Form
CLR – Commom Language Runtime
FURB – Universidade Regional de Blumenau
GLC – Gramática Livre de Contexto
IP – Instruction Point
LP – Linguagem de Programação
MS-DOS – Microsoft - Disk Operating System
MSIL – Microsoft Intermediate Language
PC – Personal Computer
PSP – Prefixo do Segmento do Programa
RF – Requisisto Funcional
RNF – Requisito Não-Funcional
SO – Sistema Operacional
TAD – Tipo Abstrato de Dado
TCC – Trabalho de Conclusão de Curso
UCP – Unidade Central de Processamento
UML – Unified Modeling Language
LISTA DE SÍMBOLOS
@ - arroba
SUMÁRIO
1 INTRODUÇÃO..................................................................................................................15
1.1 OBJETIVOS DO TRABALHO ........................................................................................16
1.2 ESTRUTURA DO TRABALHO......................................................................................16
2 FUNDAMENTAÇÃO TEÓRICA....................................................................................17
2.1 LINGUAGENS DE PROGRAMAÇÃO...........................................................................17
2.1.1 Paradigmas de linguagens de programação ....................................................................18
2.1.1.1 Paradigma imperativo...................................................................................................19
2.1.1.2 Paradigma declarativo ..................................................................................................20
2.1.2 Tipos abstratos de dados .................................................................................................21
2.2 COMPILADORES............................................................................................................23
2.2.1 Análise léxica..................................................................................................................23
2.2.2 Análise sintática ..............................................................................................................23
2.2.2.1 Gramática......................................................................................................................24
2.2.2.1.1 Gramáticas livres do contexto..................................................................................25
2.2.2.2 Análise recursiva preditiva ...........................................................................................25
2.2.3 Análise semântica............................................................................................................26
2.2.4 Tradução dirigida pela sintaxe ........................................................................................26
2.2.4.1 Esquemas de tradução...................................................................................................26
2.2.4.1.1 Gramática de atributos .............................................................................................27
2.2.4.1.2 Esquemas de tradução L-atribuídos .........................................................................28
2.2.4.2 Projeto de um tradutor preditivo...................................................................................28
2.2.5 Geração de código intermediário ....................................................................................29
2.2.6 Código intermediário de três endereços..........................................................................29
2.2.7 Gerência de memória ......................................................................................................30
2.2.8 Geração de código-alvo...................................................................................................30
2.3 MICROPROCESSADOR 8088 ........................................................................................31
2.3.1 Memória ..........................................................................................................................32
2.3.1.1 Endereços segmentados ................................................................................................32
2.3.1.2 Pilha ..............................................................................................................................33
2.3.2 Registradores...................................................................................................................33
2.3.2.1 Registradores de propósito geral ..................................................................................34
2.3.2.2 Registradores de ponteiros e de índices........................................................................34
2.3.2.3 Registradores de segmento ...........................................................................................35
2.3.2.4 Registrador de ponteiro da instrução ............................................................................35
2.3.2.5 Registrador de sinalizadores .........................................................................................36
2.3.3 Endereçando a memória por registradores......................................................................37
2.3.4 Interrupções.....................................................................................................................38
2.3.5 Conjunto de instruções do 8086/8088.............................................................................38
2.3.6 Layout de memória de um arquivo .COM.......................................................................39
2.4 FURBOL ...........................................................................................................................40
2.5 TRABALHOS CORRELATOS........................................................................................42
3 DESENVOLVIMENTO....................................................................................................44
3.1 REQUISITOS DO SISTEMA A SER DESENVOLVIDO...............................................44
3.2 ESPECIFICAÇÃO DA LINGUAGEM FURBOL ...........................................................45
3.2.1 Programa principal e bloco de comandos .......................................................................45
3.2.2 Declarações de variáveis e definições de tipos ...............................................................46
3.2.3 Procedimentos e tarefas...................................................................................................48
3.2.4 Comandos da linguagem.................................................................................................50
3.2.5 Expressões.......................................................................................................................59
3.2.6 Definições dos TADs ......................................................................................................63
3.2.7 Exemplo de um TAD definido conforme a nova especificação do FURBOL................67
3.2.8 Exemplo de uma tabela de símbolos para um programa que define e usa um TAD ......69
3.3 ASPECTOS RELEVANTES CONSIDERADOS PARA IMPLEMENTAÇÃO DA
EXTENSÃO DA LINGUAGEM PROPOSTA.................................................................72
3.3.1 Criação de objetos...........................................................................................................72
3.3.1.1 Procedimento criarobj ............................................................................................74
3.3.2 Parâmetro oculto esse ...................................................................................................74
3.3.3 Chamada de métodos ......................................................................................................75
3.3.4 Acesso aos atributos........................................................................................................77
3.3.5 Palavra reservada nulo ..................................................................................................79
3.3.6 Exclusão de objetos.........................................................................................................79
3.3.6.1 Procedimento destruirobj .....................................................................................81
3.4 ESPECIFICAÇÃO DO AMBIENTE................................................................................82
3.4.1 Diagrama de casos de uso ...............................................................................................82
3.4.2 Diagrama de classes ........................................................................................................83
3.4.3 Diagrama de seqüência para o caso de uso compilar......................................................85
3.5 IMPLEMENTAÇÃO DO AMBIENTE............................................................................87
3.5.1 Analisador léxico ............................................................................................................87
3.5.2 Analisador sintático preditivo .........................................................................................88
3.5.2.1 Exemplos da implementação de não-terminais da nova especificação da LP..............89
3.5.2.2 Erros de compilação .....................................................................................................94
3.5.2.3 Acesso aos atributos do TAD .......................................................................................94
3.6 OPERACIONALIDADE DO AMBIENTE......................................................................95
3.7 RESULTADOS E DISCUSSÃO ......................................................................................99
4 CONCLUSÕES................................................................................................................103
4.1 EXTENSÕES ..................................................................................................................103
REFERÊNCIAS BIBLIOGRÁFICAS ...............................................................................104
APÊNDICE A – Procedimentos do núcleo para criação e exclusão de objetos..............106
APÊNDICE B – Procedimento do núcleo para imprimir inteiros...................................108
15
1 INTRODUÇÃO
Uma Linguagem de Programação (LP) serve como meio de comunicação entre o
indivíduo que deseja resolver um determinado problema e o computador escolhido para
ajudá-lo na solução. Uma LP, portanto, deve fazer a ligação entre o pensamento humano e a
precisão requerida pelos computadores (PRICE; TOSCANI, 2001, p. 1). De acordo com Aho,
Sethi e Ullman (1995, p. 1), existem milhares de LPs, que vão desde linguagens tradicionais
até linguagens especializadas. O Java, o C++, o Pascal e o FURBOL são alguns exemplos de
LPs.
Para realizar a tradução de uma LP para um programa alvo ou para outra LP existem
programas especializados chamados de compiladores. “Posto de forma simples, um
compilador é um programa que lê um programa escrito numa linguagem – a linguagem fonte
– e o traduz num programa equivalente em outra linguagem – a linguagem alvo” (AHO;
SETHI; ULLMAN, 1995, p. 1).
Toda LP apresenta seu próprio conjunto de símbolos e regras para a formação e
interpretação de programas (VAREJÃO, 2004, p. 2). Algumas linguagens oferecem suporte a
Tipos Abstratos de Dados (TADs) definidos pelos usuários. Um TAD é um encapsulamento
que inclui a representação de dados de um tipo específico de dado e os subprogramas que
fornecem as operações para esse tipo. Uma instância de um TAD é chamada de objeto
(SEBESTA, 2000, p. 398).
Entre as milhares de LPs existentes pode-se citar o FURBOL. O FURBOL (acrônimo
de FURB e ALGOL) é uma LP que vem sendo desenvolvida por vários integrantes do curso
de Ciências da Computação da Universidade Regional de Blumenau (FURB) e que possui
como um dos seus grandes destaques as unidades de programas concorrentes (SILVA, 2002).
Contudo, o FURBOL atualmente ainda não oferece a possibilidade da criação de TADs
definidos pelos usuários.
Com o objetivo de ampliar a LP FURBOL, este trabalho propõe-se a estender a
mesma, a partir da implementação de Silva (2002), adicionando o suporte a TADs.
16
1.1 OBJETIVOS DO TRABALHO
O objetivo deste trabalho é estender a LP FURBOL.
O objetivo específico do trabalho é adicionar a possibilidade da criação de TADs pelos
usuários do FURBOL.
1.2 ESTRUTURA DO TRABALHO
O trabalho está dividido em quatro capítulos. No capítulo 2 é feito um levantamento
sobre os assuntos relevantes para o desenvolvimento do trabalho. O terceiro capítulo
apresenta o desenvolvimento da extensão do FURBOL para a inclusão de TADs. O quarto
capítulo contém as conclusões do trabalho, as limitações e sugestões de extensões para o
mesmo.
17
2 FUNDAMENTAÇÃO TEÓRICA
Neste capítulo são apresentadas informações sobre: LPs, compiladores, o
microprocessador 8088 e FURBOL. Na última seção são descritos alguns trabalhos correlatos.
2.1 LINGUAGENS DE PROGRAMAÇÃO
Segundo Varejão (2004, p. 1), uma LP é uma ferramenta utilizada para escrever
programas, os quais são conjuntos de instruções a serem seguidas pelo computador para
realizar um determinado processo.
Todas as LPs possuem as suas próprias características, como o seu conjunto de
símbolos e regras para a formação e interpretação de programas (VAREJÃO, 2004, p. 2).
Segundo Price e Toscani (2001, p. 1), se uma LP apresentar construções que estão mais
próximas do problema a ser resolvido, esta linguagem é considerada de alto nível. Por outro
lado, LPs que estão mais próximas das instruções entendidas pela máquina são chamadas de
linguagens de baixo nível. Como exemplos de linguagens de baixo nível podem-se citar as
linguagens de montagem (assembly) e como exemplo de LP de alto nível o Pascal.
Varejão (2004, p. 6-12) lista várias propriedades que são desejáveis em uma LP, as
quais são:
a) legibilidade: propriedade que diz respeito à facilidade para se ler e entender um
programa. Quanto mais fácil for seguir as instruções de um programa, mais fácil
será entender o que está sendo feito e descobrir erros de programação;
b) redigibilidade: essa propriedade refere-se a facilidade de escrita de um programa.
A redigibilidade de programas pode conflitar em alguns casos com a legibilidade.
Por exemplo, a LP C permite a redação de comandos complexos, mas que podem
não identificar de maneira muito clara a sua funcionalidade;
c) confiabilidade: essa propriedade relaciona-se aos mecanismos fornecidos pela LP
para incentivar a construção de programas confiáveis. LPs que possuem
mecanismos para detectar eventos indesejáveis e especificar respostas adequadas a
tais eventos permitem a construção de programas mais confiáveis. Uma LP pode,
por exemplo, verificar em tempo de execução se o índice de um vetor está dentro
18
dos limites permitidos para o vetor indexado e caso o índice esteja fora dos limites
permitidos lançar uma exceção, interrompendo o fluxo normal de execução do
programa. Essa mesma LP pode fornecer um mecanismo para capturar a exceção
lançada, permitindo que o programador trate o evento indesejável adequadamente;
d) eficiência: muito embora, hoje em dia, boa parte da responsabilidade de gerar
código eficiente seja delegado ao compilador, as características de uma LP podem
determinar se os programas escritos naquela linguagem serão mais ou menos
eficientes;
e) facilidade de aprendizado: uma LP deve ser de fácil aprendizado pelo
programador. LPs como C++ que possuem múltiplas maneiras de realizar a mesma
funcionalidade tendem a ser mais difíceis de aprender;
f) ortogonalidade: essa propriedade diz respeito à capacidade da LP permitir ao
programador combinar seus conceitos básicos, sem que se produzam efeitos
atípicos nessa combinação. Uma LP é tão mais ortogonal quanto menor for o
número de exceções aos seus padrões regulares;
g) reusabilidade: essa propriedade diz respeito à possibilidade de reutilizar o mesmo
código para diversas aplicações. Quanto mais reusável for um código, maior será a
produtividade de programação;
h) modificabilidade: propriedade que se refere às facilidades oferecidas pelas LPs
para possibilitar a alteração do programa, sem que tais modificações impliquem
mudanças em outras partes dos programas. Exemplos de mecanismos que
proporcionam boa modificabilidade são o uso de constantes simbólicas e a
separação entre interface e implementação de subprogramas;
i) portabilidade: é desejável que programas escritos em uma LP comportem-se da
mesma maneira, independente da ferramenta utilizadas para traduzí-los para a
linguagem de máquina ou o hardware e sistema operacional onde serão
executados.
2.1.1 Paradigmas de linguagens de programação
Varejão (2004, p. 17) chama de paradigma “um conjunto de características que servem
para categorizar um grupo de linguagens.”. Varejão (2004, p. 17) classifica os paradigmas das
LPs em duas categorias principais: imperativo e declarativo e adota a classificação
19
apresentada na Figura 1.
Fonte: Varejão (2004, p. 17).
Figura 1 - Paradigmas de LPs
2.1.1.1 Paradigma imperativo
Os programas escritos em uma LP que adota o paradigma imperativo especificam
como uma computação é realizada por uma seqüência de alterações no estado da memória do
computador. O foco dos programas nesse paradigma é especificar como um processamento
deve ser feito no computador (VAREJÃO, 2004, p. 17). Ainda, seguindo a classificação
apresentada na Figura 1, o paradigma imperativo é subdividido nos seguintes paradigmas
(VAREJÃO, 2004, p. 18-19):
a) paradigma estruturado: esse tipo de programação baseia-se na idéia de
desenvolvimento de programas por refinamentos sucessivos. A programação
estruturada desestimula o uso de comandos de desvio incondicional e incentiva a
divisão dos programas em subprogramas e em blocos aninhados de comandos.
Pascal é um exemplo de LP que adota o paradigma estruturado;
b) paradigma orientado a objetos: esse paradigma pode ser considerado a evolução do
paradigma estruturado. As linguagens que adotam o paradigma orientado a objetos
enfocam as abstrações de dados como elemento básico da programação. Nesse
paradigma existe o conceito de classes que são abstrações que definem uma
estrutura de dados e um conjunto de operações que podem ser realizadas sobre
elas. Outros conceitos importantes no paradigma orientado a objeto é o conceito de
herança e polimorfismo. Exemplos de LPs que adotam esse paradigma são o C++
e JAVA;
20
c) paradigma concorrente: esse paradigma engloba LPs que oferecem facilidades para
o desenvolvimento de sistemas concorrentes. A programação concorrente ocorre
quando vários processos executam simultaneamente e concorrem por recursos, tais
como dados ou dispositivos periféricos. ADA é um exemplo de LP que oferece
suporte a concorrência.
2.1.1.2 Paradigma declarativo
“Em contraste com o paradigma imperativo, no qual os programas são especificações
de como o computador deve realizar uma tarefa, no paradigma declarativo os programas são
especificações sobre o que é essa tarefa.” (VAREJÃO, 2004, p. 19). A maior preocupação do
programador que utiliza uma LP que adota este paradigma é descrever de forma abstrata a
tarefa a ser resolvida.
O paradigma declarativo é subdivido em dois paradigmas, os quais são (VAREJÃO,
2004, p. 19):
a) paradigma funcional: linguagens funcionais operam sobre funções, as quais
recebem listas de valores e retornam um valor. Um programa funcional é uma
chamada de função, que por sua vez pode chamar outras funções, para gerar um
valor de retorno. LISP e Scheme são exemplos de LPs que seguem o paradigma
funcional. No Quadro 1 é apresentado um exemplo de uma função em Scheme que
compara duas listas simples;
b) paradigma lógico: um programa lógico é composto por cláusulas que definem
predicados e relações factuais. Um diferencial do paradigma lógico é que a
execução dos programas corresponde a um processo de dedução automática.
Assim, quando uma questão é formulada, um mecanismo de inferência tenta
deduzir novos fatos a partir dos já existentes para verificar a veracidade da
questão. Como exemplo de LP que segue o paradigma lógico pode-se citar o
PROLOG. No Quadro 2 é exibido um exemplo de código em PROLOG.
21
(DEFINE (igualsimples lis1 lis2) (COND ((NULL? lis1) (NULL? lis2)) ((NULL? lis2) '()) ((EQ? (CAR lis1) (CAR lis2)) (igualsimples CDR(lis1) CDR(lis2))) (ELSE '()) )) Fonte: Sebesta (2000, p. 554).
Quadro 1 – Exemplo de uma função em Scheme
pais(X, Y) :- mãe(X, Y). pais(X, Y) :- pai(X, Y). avô(X, Z) :- pais(X, Y) , pais(Y, Z). irmãos(X, Y) :- mãe(M, X) , mãe(M, Y), pai(F, X) , pai(F, Y). Fonte: Sebesta (2000, p. 581).
Quadro 2 - Exemplo de código em PROLOG
2.1.2 Tipos abstratos de dados
A criação de tipos abstratos de dados é um recurso oferecido por várias linguagens de
programação. Para Varejão (2004, p. 168), tipos abstratos de dados “são conjuntos de valores
que apresentam um comportamento uniforme definido por um grupo de operações
(geralmente, um grupo de constantes iniciais e um conjunto de funções e procedimentos)”.
Segundo Sebesta (2000, p. 398) a abstração de dados é uma arma contra a
complexidade. É um meio de tornar programas grandes e/ou complicados mais manejáveis.
Para Varejão (2004, p. 168) com o uso de TADs o código fica mais confiável, pois o usuário
do TAD não pode efetuar livremente mudanças nos dados. Isso só pode ser feito pelas
operações do implementador do TAD. A representação interna do TAD fica “escondida” do
usuário do tipo. Além disso, desde que não haja uma alteração na interface do TAD, a sua
implementação pode ser modificada sem que o código do usuário seja também modificado.
Isso torna a manutenção dos programas menos complexa.
Para Sebesta (2000, p. 399), um tipo TAD (no contexto dos tipos definidos pelo
usuário) é um tipo de dado que satisfaz as duas condições seguintes:
a) a representação ou a definição do TAD e as operações sobre os objetos do tipo
estão contidas em uma única unidade sintática. Além disso, outras unidades de
programa podem ter permissão para criar variáveis do TAD definido;
b) a representação de objetos do TAD não é visível pelas unidades de programa que
usam o tipo, de modo que as únicas operações diretas possíveis sobre esses objetos
22
são aquelas oferecidas na definição do tipo.
Segundo Varejão (2004, p. 168), existem quatro tipos de operações que podem ser
realizadas sobre um tipo abstrato de dado, as quais são:
a) operações construtoras: usadas para inicializar o TAD;
b) operações consultoras: usadas para obter informações relacionadas com os valores
do TAD;
c) operações atualizadoras: permitem a alteração dos valores do TAD;
d) operações destrutoras: responsáveis por realizar qualquer atividade de finalização
quando o TAD não for mais necessário.
C++ é uma das linguagens existentes que oferece suporte a TAD definidos pelo
usuário. Em C++ um TAD definido pelo usuário é chamado de classe. “Os dados definidos
em uma classe são chamados membros de dados; as funções definidas em uma classe são
chamadas funções-membro” (SEBESTA, 2000, p. 407, grifo do autor). No Quadro 3 é
apresentado um exemplo de um TAD definido em C++.
class pilha { private: //** Estes membros são visíveis somente a outros membros //** e amigos int *ptr_pilha; int tam_max; int top_ptr; public: //** Estes membros são visíveis aos clie ntes pilha() { //** Um construtor ptr_pilha = new int [ 100 ]; tam_max = 99; top_ptr = -1; } ~pilha() {delete [] ptr_pilha;}; //** Um destr utor void push(int numero) { if (top_ptr == tam_max) cout << “Erro no pop – a pilha esta cheia\n ”; else ptr_pilha[++top_ptr] = numero; } void pop() { if (top_ptr == -1) cout << “Erro no pop – a pilha esta vazia\n ”; else top_ptr--; } int top() {return (ptr_pilha[top_ptr]);} int empty() {return (top_ptr == -1);} } Fonte: adaptado de Sebesta (2000, p. 408-409).
Quadro 3 - Exemplo de um TAD definido em C++
No exemplo do Quadro 3, o identificador pilha que aparece logo após a palavra
reservada class é o nome do TAD definido. Já os identificadores ptr_pilha , tam_max e
top_ptr são os membros de dados do TAD. O identificador pilha é uma função-membro
construtora, ~pilha uma função destrutora, push e pop funções atualizadoras e top e empty
23
funções consultoras.
2.2 COMPILADORES
Segundo Price e Toscani (2001, p. 4), no contexto de LPs, um tradutor é um sistema
que recebe como entrada um programa escrito em uma LP (linguagem fonte) e produz como
saída um programa equivalente em outra linguagem (linguagem objeto).
Um compilador é um tipo de programa tradutor que mapeia programas escritos em
uma LP de alto nível para programas equivalentes em linguagem simbólica (assembly) ou
linguagem de máquina (PRICE; TOSCANI, 2001, p. 4).
A tradução (compilação) de um programa é um processo que costuma ser dividido em
algumas etapas, as quais podem ser: análise léxica, análise sintática, análise semântica e
geração de código.
2.2.1 Análise léxica
A análise léxica é a primeira etapa do processo de compilação. Também denominado
de scanner, a função de um analisador léxico é “fazer a leitura do programa fonte, caractere a
caractere, e traduzí-lo para uma seqüência de símbolos léxicos, também chamados tokens.”
(PRICE; TOSCANI, 2001, p. 17).
Price e Toscani (2001, p. 17) citam como exemplos de tokens as palavras reservadas,
os identificadores, as constantes e os operadores da LP. Caracteres não significativos como
espaços em branco e comentários são desprezados pelo analisador léxico.
2.2.2 Análise sintática
Para Price e Toscani (2001, p. 29) a análise sintática é a segunda fase de um tradutor.
A sua função é verificar se as construções usadas no programa estão gramaticalmente
corretas. Segundo Louden (2004, p. 95) a sintaxe de uma LP é normalmente dada pelas regras
gramaticais de uma Gramática Livre de Contexto (GLC).
24
Dada uma GLC G e uma sentença do programa fonte s , o objetivo do analisador
sintático é verificar se a sentença s pertence à linguagem gerada por G. O analisador sintático,
também chamado de parser, recebe do analisador léxico a seqüência de tokens que forma a
sentença s e produz como resultado uma árvore de derivação para s , se a sentença é válida
para a linguagem, ou emite uma mensagem de erro, caso contrário (PRICE; TOSCANI, 2001,
p. 29).
De acordo com Price e Toscani (2001, p. 29) há duas estratégias básicas para a análise
sintática:
a) estratégia top-down ou descendente: os métodos baseados nesta estratégia
constroem a árvore de derivação a partir do símbolo inicial da gramática (raiz da
árvore), fazendo a árvore crescer até atingir suas folhas. Nesta estratégia, em cada
passo, um lado esquerdo de produção é substituído por um lado direito (expansão);
b) estratégia bottom-up ou redutiva: essa estratégia realiza a análise no sentido
inverso, ou seja, a partir dos tokens do texto fonte (folhas da árvore de derivação)
constrói a árvore até o símbolo inicial da gramática.
2.2.2.1 Gramática
Segundo Price e Toscani (2001, p. 18) uma gramática G é um mecanismo para gerar
sentenças de uma linguagem e é definida pela quádrupla: (N, T, P, S), onde N é um conjunto
de símbolos não-terminais (variáveis sintáticas), T é um conjunto de símbolos terminais
(tokens) onde (T ∩ N = ∅), P é um conjunto de regras de produção (regras sintáticas) e S é o
símbolo inicial da gramática (S ∈ N).
As regras de produção de uma gramática são representadas por α → β e definem o
mecanismo de geração de sentenças da linguagem. Uma seqüência de regras da forma α →
β1, α → β2, ..., α → βn, pode ser representada de forma simplificada por: α → β1 | β2 | ... | βn.
A aplicação sucessiva de regras de produção, a partir do símbolo inicial da gramática, permite
derivar as sentenças válidas da linguagem representada pela gramática (PRICE; TOSCANI,
2001, p. 18).
Segundo Aho, Sethi e Ullman (1995, p. 12), uma gramática deriva cadeias começando
pelo símbolo inicial da gramática e, então, substituindo repetidamente um não-terminal pelo
lado direito de uma produção para aquele não-terminal. As cadeias de tokens que podem ser
25
derivadas a partir do símbolo inicial formam a linguagem definida pela gramática.
2.2.2.1.1 Gramáticas livres do contexto
As gramáticas livres do contexto, popularizadas pela notação BNF, formam a base
para a análise sintática das LPs, pois permitem descrever a maioria das linguagens usadas
atualmente (PRICE; TOSCANI, 2001, p. 30).
De acordo com Price e Toscani (2001, p. 30) uma GLC é qualquer gramática cujas
produções são da forma: A → α onde A é um símbolo não-terminal e α é um elemento de (N ∪
T) * que é o conjunto de sentenças que podem ser formadas por N e T, incluindo a palavra
vazia Є. O nome livre do contexto deriva do fato de o não-terminal A poder ser substituído por
α em qualquer contexto, isto é, sem depender de qualquer análise dos símbolos que sucedem
ou antecedem A na forma sentencial em questão.
2.2.2.2 Análise recursiva preditiva
Um analisador sintático preditivo é um tipo de analisador sintático descendente
(PRICE; TOSCANI, 2001, p. 38).
Segundo Price e Toscani (2001, p. 41) os analisadores sintáticos preditivos exigem
que:
a) a gramática não tenha recursividade à esquerda, ou seja, um não-terminal não pode
derivar ele mesmo;
b) a gramática esteja fatorada à esquerda;
c) que para os não-terminais com mais de uma regra de produção, os primeiros
terminais deriváveis sejam capazes de identificar, univocamente, a produção que
deve ser aplicada a cada instante da análise.
O Quadro 4 apresenta o trecho de uma gramática apta a análise recursiva preditiva.
COMANDO � if EXPR then COMANDO | while EXPR do COMANDO | repeat LISTA until EXPR | id := EXPR Fonte: Price e Toscani (2001, p. 42).
Quadro 4 - Trecho de uma gramática apta a análise recursiva preditiva
Para as produções do Quadro 4, os primeiros terminais dos lados direitos (if , while ,
26
repeat e id ) determinam, diretamente, a produção a ser aplicada.
2.2.3 Análise semântica
A principal atividade de um analisador semântico é determinar se as estruturas
sintáticas analisadas fazem sentido, ou seja, verificar se um identificador declarado como
variável é usado como tal; se existe compatibilidade entre operandos e operadores em
expressões; entre outros (PRICE; TOSCANI, 2001, p. 9).
Para Louden (2004, p. 259) a análise semântica pode ser dividida em duas categorias.
A primeira é a análise de um programa requerida pelas regras da LP, para verificar sua
correção e garantir sua execução. A outra categoria é aquela efetuada pelo compilador para
melhorar a eficiência de execução do programa traduzido (otimização).
2.2.4 Tradução dirigida pela sintaxe
Tradução dirigida pela sintaxe é uma técnica que permite realizar a tradução do
programa (geração de código) simultaneamente a análise sintática. Nesta técnica ações
semânticas (envolvidas entre chaves) são associadas às regras de produção da gramática de
modo que, quando uma dada produção é processada, essas ações são executadas (PRICE;
TOSCANI, 2001, p. 85).
2.2.4.1 Esquemas de tradução
Segundo Price e Toscani (2001, p. 86) um esquema de tradução é uma extensão de
uma GLC que associa atributos aos símbolos gramaticais e de ações semânticas às regras de
produção. As ações semânticas podem ser avaliações de atributos ou chamadas a
procedimentos e funções. No Quadro 5 é apresentado um exemplo de esquema para traduzir
expressões infixadas para expressões pós-fixadas.
27
E � T R R � Op T { print(Op.simbol) } R | Є T � num { print(num.lexval)}
Fonte: Price e Toscani (2001, p. 88). Quadro 5 - Exemplo de um esquema de tradução
Louden (2004, p. 261) apresenta alguns exemplos típicos de atributos, os quais são:
a) tipo de dados de uma variável;
b) valor de uma expressão;
c) localização de uma variável na memória;
d) código-objeto de um procedimento;
e) quantidade de dígitos significativos em um número.
Os atributos de um esquema de tradução são classificados em:
a) atributos sintetizados: um atributo é classificado como sintetizado se todas as suas
dependências apontarem de filho para pai na árvore de análise sintática (estrutura
de dados utilizada para representação da estrutura sintática de uma linguagem)
(LOUDEN, 2004, p. 279);
b) atributos herdados: são aqueles cujo valor são computados a partir dos valores do
nó pai e/ou irmãos na árvore de análise sintática (AHO; SETHI; ULLMAN, 1995,
p. 120).
2.2.4.1.1 Gramática de atributos
As ações semânticas de um esquema de tradução podem produzir efeitos colaterais,
tais como, imprimir um valor, armazenar um literal em uma tabela ou atualizar uma variável
global. Quando o esquema de tradução não produz efeitos colaterais ele é dito ser uma
gramática de atributos. Nesse caso, as ações semânticas são atribuições ou funções
envolvendo, exclusivamente, os atributos do esquema de tradução (PRICE; TOSCANI, 2001,
p. 89). No Quadro 6 é apresentado um exemplo de uma gramática de atributos.
A � A 1 digit { A.val := A 1.val + digit.lexval }
A � digit { A.val := digit.lexval }
Fonte: Price e Toscani (2001, p. 90). Quadro 6 - Exemplo de uma gramática de atributos
No exemplo apresentado, val é um atributo do símbolo não-terminal A, enquanto
lexval é um atributo do símbolo terminal digit . A presença do índice 1 em A1 serve para
diferenciar as duas ocorrências de A. Isso é necessário, pois o atributo val será diferente para
28
cada caso de A. O conteúdo entre chaves é a ação semântica a ser executada.
2.2.4.1.2 Esquemas de tradução L-atribuídos
Os esquemas de tradução L-atribuídos restringem o uso de atributos herdados, de
modo a permitir que as ações semânticas possam ser executadas em um único passo durante a
análise sintática. Basicamente a restrição imposta é “para cada símbolo X no lado direito de
uma regra de produção, a ação que calcula um atributo herdado de X deve aparecer à esquerda
de X” (PRICE; TOSCANI, 2001, p. 94).
2.2.4.2 Projeto de um tradutor preditivo
Um tradutor preditivo é um programa que possui um procedimento para cada não-
terminal. Cada procedimento decide que produção usar e implementa o lado direito das
produções (AHO; SETHI; ULLMAN, 1995, p. 21). O seu algoritmo é apresentado no Quadro
7.
Entrada: um esquema de tradução (o esquema deve ser baseado numa gramática apropriada para análise preditiva). Resultado: código de um tradutor dirigido por sintaxe. Método: 1) Para cada não-terminal A, construa uma função que tenha um parâmetro formal para cada atributo herdado de A e que retorne os valores dos atributos sintetizados de A. Além disso, a função para A deve ter uma variável local para cada atributo de símbolo gramatical que aparece nas produções de A. 2) O código para o não-terminal A deve decidir qual produção usar, baseado no símbolo corrente da entrada. Considerando o lado direito da produção a ser usada, da esquerda para a direita, o código associado deve implementar o seguinte: i) para cada token X, verificar se X é o token lido e avançar a leitura; ii) para cada não-terminal B, gerar uma atribuição c := B(b1, b2, ..., bk), sendo b1, b2, ..., bk variáveis para os atributos herdados de B, e c a variável para o atributo sintetizado de B; iii) para cada ação semântica, copiar o código correspondente, substituindo cada referência a atributo pela variável desse atributo. Fonte: adaptado de Price e Toscani (2001, p. 110).
Quadro 7 - Algoritmo para a construção de um tradutor preditivo dirigido pela sintaxe
29
2.2.5 Geração de código intermediário
Price e Toscani (2001, p. 115) definem a geração de código intermediário como sendo
a transformação da árvore de derivação em um segmento de código que pode, eventualmente,
ser o código objeto final, mas que na maioria das vezes, constitui-se num código
intermediário.
De acordo com Aho, Sethi e Ullman (1995, p. 200), apesar de um compilador poder
traduzir o programa-fonte diretamente na linguagem-alvo, existem alguns benefícios em se
usar uma forma intermediária de código independente de máquina. Price e Toscani (2001, p.
115) listam alguns benefícios da geração de código intermediário, os quais são:
a) possibilita a otimização do código intermediário;
b) simplifica a implementação do compilador, resolvendo, gradativamente, as
dificuldades da passagem de código fonte para objeto (alto-nível para baixo-nível);
c) possibilita a tradução do código intermediário para diversas máquinas.
2.2.6 Código intermediário de três endereços
De acordo com Price e Toscani (2001, p. 117), no código intermediário de três
endereços, cada instrução faz referência no máximo a três variáveis (endereços de memória)
(Quadro 8).
A := B op C A := op B A := B goto L if A oprel B goto L
Fonte: Price e Toscani (2001, p. 117). Quadro 8 - Instruções do código de três endereços
No exemplo do Quadro 8, A, B e C representam endereços de variáveis, op representa
um operador binário (primeira instrução) ou unário (segunda instrução), oprel representa um
operador relacional e L representa o rótulo de uma instrução.
30
2.2.7 Gerência de memória
De acordo com Price e Toscani (2001, p. 169-170) a memória para um programa
compilado deve conter:
a) o código objeto gerado;
b) espaço para a área estática;
c) a pilha para ativação dos procedimentos;
d) espaço para a memória dinâmica (heap).
A reserva de memória para os dados de um programa pode ser feita de três maneiras,
as quais são (PRICE; TOSCANI, 2001, p. 170):
a) alocação estática: quando o comprimento de um dado é conhecido durante a
compilação e não é alterado durante a execução do programa, o compilador pode
gerar diretivas para que a reserva de memória para esse dado seja feita na
inicialização do programa;
b) alocação em pilha: embora muitas vezes o espaço necessário para a chamada de
um procedimento, tais como, variáveis locais, parâmetros e endereço de retorno,
seja conhecido em tempo de compilação, a alocação desse espaço somente pode
ser feita em tempo de execução, pois a ordem de chamadas é determinada pela
execução do programa. Portanto, a alocação deve ser feita dinamicamente e, em
geral, é realizada numa estrutura em pilha, a qual pode ser a própria pilha de
execução da máquina;
c) alocação dinâmica: para as estruturas de dados referenciadas através de ponteiros,
as áreas também são reservadas dinamicamente. Essas áreas são alocadas e
liberadas através de comandos específicos da LP, tais como new e dispose da
linguagem Pascal. Normalmente, essas estruturas são alocadas em uma área
chamada de heap, que cresce no sentido contrário ao da pilha.
2.2.8 Geração de código-alvo
Segundo Aho, Sethi e Ullman (1995, p. 222-223), a geração de código alvo é a fase
final de um compilador. Essa recebe como entrada a representação intermediária do
programa-fonte e produz como saída um programa-alvo equivalente. Esta saída pode assumir
31
uma variedade de formas, tais como, linguagem absoluta de máquina, linguagem relocável de
máquina ou linguagem de montagem (assembly).
Price e Toscani (2001, p. 186) apresentam quatro aspectos que devem ser considerados
no projeto de geradores de código, os quais são:
a) forma do código objeto a ser gerado: linguagem absoluta de máquina, relocável ou
de montagem;
b) seleção das instruções de máquina: a escolha da seqüência apropriada das
instruções pode resultar em um código mais otimizado;
c) alocação de registradores;
d) escolha da ordem de avaliação das instruções.
Para Aho, Sethi e Ullman (1995, p. 224), “Uma familiaridade com a máquina-alvo e
seu conjunto de instruções é um pré-requisito para o projeto de um bom gerador de código”.
A máquina-alvo para a qual o FURBOL gera código alvo é o microprocessador 8088 (SILVA,
2002), o qual é apresentado na seção seguinte.
2.3 MICROPROCESSADOR 8088
O 8088 é um microprocessador desenvolvido pela empresa Intel Corporation, sendo
uma versão do modelo 8086 (SANTOS; RAYMUNDI JÚNIOR, 1989, p. 8).
De acordo com Norton e Wilton (1991, p. 6), o microprocessador 8086 difere do 8088
em apenas um pequeno aspecto: ele usa o barramento de dados inteiro de 16 bits ao invés da
barramento de 8 bits usado pelo 8088. Praticamente tudo o que se lê sobre o 8086 também se
aplica ao 8088; para fins de programação pode-se considerá-los idênticos. Portanto, neste
trabalho 8086 e 8088 serão tratados como sinônimos.
Em todos os Personal Computers (PCs), o microprocessador é o chip que roda os
programas. O microprocessador, ou Unidade Central de Processamento (UCP), executa uma
série de cálculos, comparações numéricas e transferências de dados em respostas aos
programas armazenados na memória. A UCP controla a operação básica do computador,
emitindo e recendo sinais de controle, endereços de memória e dados de uma parte do
computador para outra, ao longo de uma rede de vias eletrônicas interconectadas chamada de
barramento. Ao longo do barramento encontram-se as portas de entrada e saída, que conectam
os vários dispositivos de memória e de suporte ao barramento (NORTON; WILTON, 1991, p.
32
2).
2.3.1 Memória
Segundo Santos e Raymundi Júnior (1989, p. 8), a memória em um sistema baseado no
8086/8088 é uma seqüência de no máximo 1.048.576 bytes (1 Mbyte). A cada byte associa-se
um endereço, na faixa de 00000 a FFFFF, em notação hexadecimal.
Como o 8086/8088 é um microprocessador de 16 bits, esse não pode trabalhar
diretamente com números mais largos que 16 bits. Teoricamente, isso significa que o
8086/8088 é capaz de acessar somente 64 KB de memória. Contudo, na prática ele pode
acessar 1 Mbyte de memória. Isso é possível porque o esquema de endereçamento de 20 bits,
usado com o 8086/8088, pode trabalhar de 216 (65.536) para 220 (1.048.576). Ainda, o
8086/8088 é limitado por sua capacidade de armazenamento de 16 bits. Para acessar
endereços de 20 bits o 8086/8088 utiliza um método de endereçamento segmentado
(NORTON; WILTON, 1991, p. 23).
2.3.1.1 Endereços segmentados
Segundo Norton e Wilton (1991, p. 23-24), o 8086/8088 divide o espaço de memória
endereçável em segmentos, cada um contendo 64 KB de memória. Cada segmento começa em
um endereço de parágrafo – ou seja, um local de byte que é divisível exatamente por 16. Para
acessar bytes ou palavras (dois bytes consecutivos na memória) individuais, usa-se um
deslocamento (offset) que aponte para o local de byte exato dentro de um segmento particular.
O microprocessador converte um endereço segmentado qualquer em um endereço físico de 20
bits, usando o valor de segmento como número de parágrafo e somando o valor de
deslocamento a ele. Com efeito, o microprocessador desloca o valor de segmento à esquerda
por 4 bits e depois soma o valor de deslocamento para criar um endereço de 20 bits.
33
2.3.1.2 Pilha
Para Santos e Raymundi Júnior (1989, p. 30-31) a pilha é uma característica inerente
aos microprocessadores. A pilha de um microprocessador é uma área de memória usada para
guardar dados, normalmente valores presentes nos registradores que precisam ser alterados,
mas que devem depois recuperar seu antigo valor. As instruções que fazem isso são PUSH
(adiciona um valor no topo da pilha), POP (recupera uma valor do topo da pilha), PUSHF
(adiciona o valor do registrador FLAGS no topo da pilha) e POPF (recupera o valor do
registrador FLAGS do topo da pilha). Uma característica curiosa e que deve ser notada é que a
pilha do 8086/8088 tem sua base em um endereço de memória alto dentro do segmento de
pilha, crescendo em direção à memória de endereço baixo.
Norton e Wilton (1991, p. 30) afirmam que o uso mais importante da pilha é na
manutenção do registro do local de onde as sub-rotinas foram chamadas e quais os parâmetros
passados a ela. A pilha também pode ser usada como área de armazenamento das variáveis
locais de uma sub-rotina.
2.3.2 Registradores
De acordo com Norton e Wilton (1991, p. 26), o 8086/8088 foi projetado para executar
instruções e operações lógicas e aritméticas na hora em que recebe as instruções e transfere
dados de ou para a memória. Para fazer isso ele usa quatorze registradores de 16 bits, cada
qual com uma finalidade especial. Santos e Raymundi Júnior (1989, p. 10) dividem os
registradores em cinco grupos, os quais são:
a) registradores de propósito geral;
b) registradores de ponteiros e de índice;
c) registradores de segmento;
d) registrador de ponteiro da instrução;
e) registrador de sinalizadores.
No Quadro 9 são apresentados os registradores do microprocessador 8086/8088.
34
Registradores de propósito geral AX, BX, CX e DX
Registradores de ponteiros e de índices BP, SP, SI, DI
Registradores de segmento CS, DS, SS, ES
Registrador de ponteiro de instrução IP
Registrador de sinalizadores FLAGS
Quadro 9 - Registradores do 8086/8088
2.3.2.1 Registradores de propósito geral
As metades alta e baixa dos quatro registradores de propósito geral também podem ser
usadas como registradores de 8 bits. Assim têm-se os seguintes registradores de 8 bits: AL e AH
(parte baixa e alta do registrador AX), BL e BH, CL e CH, DL e DH (SANTOS; RAYMUNDI
JÚNIOR, 1989, p. 12).
Segundo Santos e Raymundi Júnior (1989, p. 12), com relação ao uso dos registradores
de propósito geral têm-se uma grande liberdade na escolha de qual registrador usar para
realizar operações aritméticas e lógicas. Contudo, existem instruções que os usam com certas
funções especializadas, como as instruções de multiplicação e divisão que se concentram nos
registradores AL e AX. Devido aos seus usos especializados em algumas instruções, os
registradores de propósito geral recebem os seguintes nomes descritivos:
a) AX é o acumulador, por concentrar resultados em algumas instruções aritméticas;
b) BX é chamado de base, pois é o único registrador de propósito geral que pode ser
usado para gerar um endereço de memória para acessar dados dentro do segmento
de dados;
c) CX é o contador, por conter, normalmente, o número de vezes que se quer repetir
um laço;
d) DX é chamado de registrador de dados, por ser usado para especificar o endereço
nas instruções de entrada e saída.
2.3.2.2 Registradores de ponteiros e de índices
Os registradores deste grupo são usados para armazenar endereços de deslocamento
35
(offset) dentro dos segmentos, onde os dados devem ser acessados. O par SP e BP trabalha
dentro do segmento da pilha, enquanto o par SI e DI trabalha normalmente, no segmento de
dados (SANTOS; RAYMUNDI JÚNIOR, 1989, p. 13).
Santos e Raymundi Júnior (1989, p. 13-14) afirmam que embora os registradores de
ponteiros e de índices tenham seus usos especializados, pode-se empregar os registradores BP,
SI e DI em operações aritméticas e lógicas, sempre de 16 bits. Já o uso do SP nessas
operações não é recomendável, pois a perda do indicador do topo da pilha pode acarretar
efeitos desastrosos para um programa.
2.3.2.3 Registradores de segmento
Os registradores de segmento (CS, DS, SS e ES) são usados para gerar endereços de
memória de 20 bits, conforme visto na seção 2.3.1.1. Identificam os quatro segmentos
endereçáveis naquele momento pelo programa. Cada segmento identifica um bloco de
memória de 64 Kbytes (SANTOS; RAYMUNDI JÚNIOR, 1989, p. 14).
Segundo Norton e Wilton (1991, p. 28) cada registrador de segmento é usado para um
tipo especial de endereçamento, os quais são:
a) o registrador CS identifica o segmento de código, que contém o programa sendo
executado;
b) os registradores DS e ES identificam os segmentos de dados onde os dados usados
são armazenados;
c) o registrador SS identifica o segmento de pilha, que foi apresentada na seção
2.3.1.2.
2.3.2.4 Registrador de ponteiro da instrução
De acordo com Santos e Raymundi Júnior (1989, p. 14), os códigos de todas as
instruções são buscados no segmento de código. O registrador ponteiro da instrução
(Instruction Point – IP) indica o deslocamento dentro do segmento de código onde está a
instrução a ser executada.
Norton e Wilton (1991, p. 29-30) afirmam que “os programas não possuem acesso
36
direto ao registrador IP , mas há uma série de instruções, como JMP e CALL, que alteram
indiretamente o valor de IP , ou salvam e recuperam o seu valor da pilha.”
2.3.2.5 Registrador de sinalizadores
Segundo Santos e Raymundi Júnior (1989, p. 15) o 8086/8088 contém em seu interior
um total de nove sinalizadores (flags). O seu propósito é indicar resultados obtidos sempre na
última operação aritmética ou lógica executada, ou para definir o comportamento do
microprocessador na execução de certas instruções. Esses 9 flags estão agrupados em um
registrador de 16 bits, sendo que apenas 9 bits são utilizados. Esse registrador é chamado de
FLAGS. O significado de cada bit do registrador é apresentador no Quadro 10.
FLAG Propósito
O (Overflow) Indica overflow do bit de alta ordem (bit mais significativo) após uma operação aritmética. Este overflow deve ser entendido como um “estouro” na capacidade do byte em armazenar um valor sinalizado.
D (Direction) Se o valor desse bit for 0, após a execução de uma instrução string, os registradores de índice envolvidos serão incrementados automaticamente e, em caso contrário, serão decrementados. Há apenas duas instruções que manipulam este flag (CLD e STD).
I (Interrupt) Indica se as interrupções estão ou não habilitadas. O valor 1 indica que as interrupções estão habilitadas, isto é, se ocorrerem serão atendidas. O valor 0 indica que as mesmas estão desabilitadas. Pode-se definir a opção desejada com as instruções CLI e STI.
T (Trap) Permite a operação “passo a passo”.
S (Sign) Contém o sinal resultante após uma operação aritmética (0 = positivo, 1 = negativo). Este sinalizador tem relevância quando usa-se números sinalizados.
Z (Zero) Indica o resultado da operação aritmética ou de comparação (0 = não zero, 1 = resultado zero, ou igual).
A (Aux. carry) Indica o transporte do bit 3 em um dado de 8 bits. Esse flag tem uso restrito a operações aritméticas com valores Binary-Coded Decimal (BCD, em português Código Binário Decimal).
P (Parity) Indica a paridade dos 8 bits de baixa ordem (1 = par, 0 = ímpar) de um resultado, após uma instrução aritmética ou lógica.
C (Carry) Indica o transporte do bit de mais alta ordem, após uma operação aritmética, ou contém o último bit, após uma operação de rotação ou deslocamento.
Fonte: adaptado de Santos e Raymundi (1989, p. 16-17). Quadro 10 - Propósito dos sinalizadores do registrador FLAGS
37
2.3.3 Endereçando a memória por registradores
Em Norton e Wilton (1991, p. 32-33) são apresentadas várias formas de se endereçar
os dados da memória através dos registradores da UCP, as quais são apresentadas no Quadro
11.
Nome Endereço Efetivo Exemplo Comentários
imediato valor “endereçado” faz parte da instrução do 8086/8088
mov ax, 1234h Armazena 1234H em AX.
direto especificado como parte da instrução do 8086/8088
mov ax, [1234h] Copia o valor encontrado no endereço com deslocamento 1234h para o registrador AX. O registrador de segmento default é DS.
indireto contido em BX, SI, DI ou BP
mov ax, [bx] Copia o valor no deslocamento contido em BX para AX. O registrador de segmento default para [BX], [SI] e [DI] é DS; para [BP] o default é SS.
base a soma de um deslocamento (parte da instrução) e o valor em BX ou BP
mov ax, [bx+2] ou mov ax, 2[bx]
Copia o valor 2 bytes além do deslocamento contido em BX para AX. O registrador de segmento default para [BX] é DS; para [BP] o default é SS.
indexado a soma de um deslocamento e o valor em SI ou DI
mov ax, [si+2] ou mov ax, 2[si]
Copia o valor 2 bytes além do deslocamento contido em SI para AX. O registrador de segmento default é DS.
indexado na base
a soma de um deslocamento, o valor em SI ou DI e o valor em BX ou BP
mov ax, [bp+si+2] ou mov ax, 2[bp+si] ou mov ax, 2[bp][si]
O deslocamento é a soma dos valores em BP e SI, mais 2. Quando BX é usado, o registrador de segmento default é DS; quando BP é usado, o default é SS.
cadeia cadeia fonte: registrador indireto usando SI Cadeia destino: registrador indireto usando DI
movsb Copia a cadeia da memória em DS:[SI] para ES:[DI].
Fonte: adaptado de Norton e Wilton (1991, p. 32-33). Quadro 11 - Modos de endereçamento do 8086/8088
38
2.3.4 Interrupções
De acordo com Norton e Wilton (1991, p. 37) uma interrupção é uma indicação para
UCP de que sua atenção imediata é necessária. O 8086/8088 pode responder a interrupções de
software e hardware. Quando uma interrupção é gerada (no caso da interrupção de software
através da instrução INT) a UCP salva na pilha o conteúdo corrente do registrador de flags, do
registrador CS e do registrador IP. Em seguida ela transfere o controle ao manipulador de
interrupção. O manipulador de interrupção termina a sua rotina com uma instrução IRET que
retorna CS:IP e os flags aos registradores correspondente, transferindo assim o controle de
volta ao programa interrompido.
2.3.5 Conjunto de instruções do 8086/8088
A linguagem de montagem (assembly) do 8086/8088 é composta por uma série de
códigos simbólicos de instrução (mnemônicos). Um montador (assembler) traduz essas
instruções e dados associados em forma binária, chamada linguagem de máquina, que pode
residir na memória e ser processada pelo 8086/8088 para realizar tarefas especificas
(NORTON; WILTON, 1991, p. 18). No Quadro 12 são apresentadas as instruções mais
usadas na implementação deste trabalho.
39
Instrução Formato Propósito
CALL CALL alvo Chamar uma sub-rotina.
INT INT tipo Alterar o fluxo de execução do programa, desviando-se para uma rotina de interrupção.
LEA LEA destino, fonte Carregar o endereço de offset, ou deslocamento de um operando na memória para o registrador de 16 bits especificado em destino.
MOV MOV destino, fonte Copiar o conteúdo do operando fonte para o destino.
POP POP destino Retirar a palavra armazenada no topo da pilha colocando-a no registrador ou posição de memória especificada por destino.
PUSH PUSH fonte Colocar no topo da pilha o valor de fonte.
RET RET [dado] Encerrar uma sub-rotina, transferindo o fluxo do processamento para a instrução seguinte à chamada da sub-rotina. O valor [dado] é opcional. Nele pode-se especificar um valor que após o retorno da sub-rotina será adicionado ao SP.
Fonte: adaptado de Santos e Raymundi (1989, p. 33-92). Quadro 12 - Conjunto de instruções do 8086/8088
2.3.6 Layout de memória de um arquivo .COM
Segundo Santos e Raymundi Júnior (1989, p. 171) os arquivos .COM contêm apenas
um segmento definido. Esse contém todos os dados do programa, inclusive o Prefixo do
Segmento do Programa (PSP) e a pilha. Isto é feito criando-se um segmento para o código e,
através do pseudo-operador do montador Assume, fazer com que os quatros registradores de
segmento referenciem o mesmo bloco físico de 64 Kbytes. O Sistema Operacional (SO) cria o
PSP e o inclui nos primeiros 256 bytes do segmento. Deste modo, o ponto de entrada para o
programa deve ser definido em 100H.
O Quadro 13 apresenta o layout de memória de um arquivo .COM.
40
CS:0000H PSP
CS:0100H código do programa e dados
↓
↑
pilha Quadro 13 - Layout de memória de um arquivo .COM
O Quadro 14 apresenta a estrutura completa de um arquivo .COM em linguagem de
montagem (assembly).
CODIGO SEGMENT PARA ‘CODE’ ASSUME CS:CODIGO, SS:CODIGO, DS:CODIGO, ES:CODIGO ORG 100H INICIO: JMP COMECO | :
definição de variáveis : : COMECO PROC NEAR : :
corpo de instruções : : ret COMECO ENDP CODIGO ENDS END INICIO
Fonte: adaptado de Santos e Raymundi (1989, p. 172). Quadro 14 - Estrutura completa de um arquivo .COM
2.4 FURBOL
O FURBOL é uma LP que adota o paradigma imperativo e vem sendo desenvolvida
por vários integrantes do curso de Ciências da Computação da FURB (VARGAS, 1992, 1993;
SILVA; SILVA, 1993; BRUXEL, 1996; RADLOFF, 1997; SCHMITZ, 1999; ANDRÉ, 2000;
ADRIANO, 2001; SILVA, 2002). Atualmente o FURBOL não é mais apenas uma LP. Ele
consiste de um ambiente integrado de desenvolvimento, onde é possível editar o código fonte
41
em FURBOL e compilá-lo, gerando como saída um código de montagem (assembly)
compatível com microprocessadores 8086/8088.
Algumas características da linguagem FURBOL são:
a) comandos em língua portuguesa;
b) comando de condição e repetição;
c) unidades de programas concorrentes;
d) mecanismo de sincronização da comunicação entre unidades concorrentes do tipo
semáforo.
O compilador do FURBOL é um tradutor dirigido pela sintaxe que utiliza o método de
análise gramatical preditiva, onde a sintaxe é definida utilizando uma gramática livre de
contexto apresentada na BNF e a semântica utilizando gramática de atributos (SILVA, 2002).
Na Figura 2 é exibida a interface do FURBOL com um programa exemplo.
Fonte: Silva (2002, p. 137).
Figura 2 - Interface do FURBOL
Em Silva (2002) é descrito o desenvolvimento de unidades de processos concorrentes
para a linguagem de programação FURBOL. No trabalho é implementado um escalonador de
tarefas que é instalado na rotina de tratamento da interrupção de hardware 8H (timer). Para
permitir a sincronização das tarefas foi implementado o mecanismo de sincronização do tipo
semáforo. No Quadro 15 é apresentado um exemplo de programa em FURBOL que utiliza
42
unidades de processos concorrentes.
programa produtor_consumidor; var cheio, vazio: semaforo; tarefa produtor; var c: inteiro; inicio c := 4; enquanto c > 0 faca inicio imprime("P -> Produzindo..."); p(vazio); imprime("P -> Armazenado!"); v(cheio); dec(c); fim ; fim ; tarefa consumidor; var c: inteiro; inicio c := 4; enquanto c > 0 faca inicio p(cheio); imprime("C -> Retirado!"); v(vazio); imprime("C -> Consumindo..."); dec(c); fim ; fim ; inicio criarsmf(cheio, 0); criarsmf(vazio, 2); produtor; consumidor; espera ; excluirsmf(cheio); excluirsmf(vazio); fim.
Fonte: Silva (2002, p. 109). Quadro 15 - Programa em FURBOL com unidades de processos concorrentes
2.5 TRABALHOS CORRELATOS
Tomazelli (2004) apresenta o desenvolvimento de um compilador para uma LP com
geração de código Microsoft Intermediate Language (MSIL). A LP apresentada é
simplificada, com comandos em português e herda algumas características estruturais da
linguagem C. O resultado do processo de compilação é um arquivo texto com código MSIL
(Quadro 16b), o qual é lido pelo montador ILAsm da Microsoft, gerando um módulo
gerenciado. Esse módulo gerenciado (arquivo “.EXE”) pode então ser executado pela
43
Commom Language Runtime (CLR).
Exemplo Código MSIL programa teste { vazio principal() { inteiro n = 0; enquanto (n <= 10) { escreva(n * 2, "\n"); n += 1; } } }
(a)
.assembly extern mscorlib { }
.assembly teste { }
.module teste.exe .class public teste { .method public static void principal() { .entrypoint .locals (int32 V_0) ldc.i4 0 stloc V_0 lb000000: ldloc V_0 ldc.i4 10 cgt ldc.i4 0 ceq brfalse lb000001 { ldloc V_0 ldc.i4 2 mul call void [mscorlib]System.Console::Write(int32) ldstr "\n" call void [mscorlib]System.Console::Write(string) ldloc V_0 ldc.i4 1 add stloc V_0 } br lb000000 lb000001: ret } }
(b)
Fonte: Tomazelli (2004, p. 46). Quadro 16 - Exemplo de programa na linguagem implementada por Tomazelli (2004)
Leyendecker (2005) apresenta a especificação de uma LP orientada a objetos (Quadro
17) para a plataforma Microsoft .NET e o desenvolvimento de um compilador para esta
linguagem. Assim como em Tomazelli (2004), no final do processo de compilação o
montador de MSIL da Microsoft (o ILAsm) é invocado com o intuito de gerar um arquivo
executável ou uma biblioteca de classes para a plataforma .NET.
001 namespace org.furb.pacote1; 002 003 using org.furb.pacote2.*; 004 using org.furb.pacote3.Classe1; 005 006 public class ClasseQualquer 007 extends ClasseBase { 008 //Definição interna da classe 009 }
Fonte: Leyendecker (2005, p. 42). Quadro 17 - Exemplo da definição de uma classe na linguagem especificada em Leyendecker (2005)
44
3 DESENVOLVIMENTO
Este capítulo tem como objetivo apresentar as etapas envolvidas no desenvolvimento
do trabalho proposto. Serão abordadas as seguintes etapas: requisitos do sistema,
especificação da LP FURBOL, aspectos relevantes considerados para implementação da
extensão da linguagem proposta, especificação, implementação, operacionalidade do sistema
e resultados e discussão.
Observa-se ainda que este capítulo limita-se a descrever o desenvolvimento de TADs.
Especificações usadas e não descritas aqui foram reaproveitadas de Silva (2002). A
documentação destas funções é encontrada também em Silva (2002).
3.1 REQUISITOS DO SISTEMA A SER DESENVOLVIDO
A ferramenta deverá:
a) manter os seguintes requisitos do ambiente de Silva (2002):
- possuir um editor para os arquivos fonte (Requisito Funcional – RF),
- compilar o código fonte FURBOL, gerando como saída código de montagem
(assembly) compatível com microprocessadores 8086/8088 (RF),
- emitir mensagens com os resultados da compilação (RF),
- permitir a visualização do código intermediário de três endereços gerado (RF),
- permitir a visualização do código de montagem (assembly) gerado (RF),
- invocar o montador Turbo Assembler e o ligador Turbo Link para a geração de
código de máquina executável em microprocessadores 8086/8088 (RF);
b) permitir a definição de TADs pelos usuários (RF);
c) usar esquemas de tradução para representar a semântica (Requisito Não-Funcional
- RNF);
d) ser implementado usando o ambiente Borland Delphi 7.0 (RNF).
45
3.2 ESPECIFICAÇÃO DA LINGUAGEM FURBOL
Nesta seção é apresentada a especificação da LP FURBOL. Ela é uma extensão da
especificação apresentada em Silva (2002), na qual foram incluídas regras e ações semânticas
para oferecer suporte a TADs definidos pelos usuários. As extensões ou modificações feitas
na gramática estão sublinhadas, quando de sua apresentação em quadros.
Os símbolos em negrito são símbolos terminais da gramática. As palavras entre
apóstrofes (‘’) também são consideradas como símbolos terminais, podendo ser palavras
reservadas ou qualquer outro token. As palavras em itálico DeclaraDadosGlobais, EmiteInter
e EmiteObjeto geram diretivas para que seja realizada a reserva de memória para as variáveis
globais, a saída do compilador em código de três endereços e a saída do compilador em
linguagem de montagem (assembly), respectivamente.
A palavra Simbolos representa o gerenciador de símbolos (interface entre o analisador
sintático e os símbolos onde a instalação, a busca, a atualização dos símbolos e o
gerenciamento de escopo são realizados). A palavra CRLF (do inglês Carriage Return Line
Feed) representa uma quebra de linha ou nova linha. As demais palavras são consideradas
elementos não-terminais (como por exemplo, EstruturaDados, ComandoComposto¸ entre
outros), os quais possuem derivações. A palavra vazia é representada pelo símbolo
circunflexo (^).
A palavra GeraCodigoAssembly é uma função que calcula o endereço de um símbolo e
retorna-o num formato de operando assembly que endereça um dado na memória.
Os atributos codigo e codasm contêm o código intermediário e o código assembly
respectivamente. O símbolo “||” é utilizado para representar a concatenação dos enunciados e
atributos que armazenam os códigos na medida em que são gerados.
3.2.1 Programa principal e bloco de comandos
A definição do programa e dos blocos de comandos é mostrada no Quadro 18. As
palavras CarregaProcsNucleo e CarregaIniFinalNucleo representam a carga dos
procedimentos do núcleo e o código de inicialização e finalização, respectivamente. Ambas as
palavras são agrupamentos de chamadas seqüenciais da função CarregaRotina que carrega
um arquivo que contém o código da rotina passada no parâmetro.
46
Programa → 'programa' EmiteObjeto('.8086'); EmiteObjeto('codigo_segmento segment'); EmiteObjeto('assume cs:codigo_segmento,' || 'ds:codigo_segmento,ss:nothing,es:nothing'); EmiteObjeto('org 100h');
id EmiteInter(id.nome || '0'); EmiteInter('goto ' || id.nome); EmiteObjeto('entrada: jmp ' || id.nome);
';' Simbolos.AbrirEscopo('Principal');
DefinicoesTADs
EstruturaDados
EstruturaSubRotinas
CComposto Programa.codigo := DefinicoesTADs.codigo || CRLF || EstruturaSubRotinas.codigo || CRLF || 'principal proc near' || CRLF || CComposto.codigo || CRLF || 'ret' || CRLF || 'endp' || CRLF || id.nome || ':' || CRLF || 'int 8h' || ':' || CRLF || 'int 20h' || CRLF Programa.codasm := DefinicoesTADs.codasm || CRLF || EstruturaSubRotinas.codasm || CRLF || 'principal_proc proc near' || CRLF || 'push bp' || CRLF || 'mov bp,sp' || CRLF || CComposto.codasm || CRLF || 'pop bp' || CRLF || 'principal_proc endp' || CRLF || CarregaProcsNucleo || CRLF || id.nome || ':' || CRLF || CarregaIniFinalNucleo || CRLF EmiteInter(Programa.codigo); EmiteObjeto(Programa.codasm);
'.' DeclaraDadosGlobais; EmiteObjeto('codigo_segmento ends'); EmiteObjeto('end entrada');
CComposto → 'inicio'
Comando CComposto.codigo := Comando.codigo; CComposto.codasm := Comando.codasm;
'fim'
Quadro 18 - Definição do programa principal e blocos
Com a extensão realizada neste trabalho tornou-se possível definir TADs logo após a
identificação do programa, acima das declarações das variáveis globais.
3.2.2 Declarações de variáveis e definições de tipos
A definição da declaração das variáveis é mostrada no Quadro 19.
47
EstruturaDados → 'var'
id Se não Simbolos.SimboloRedeclarado(id.nome) então Simbolos.Instalar(TSimbolo.Create(id.nome));
ListaID (Matriz | ^)
Simbolos.AtualizarUltimosSimbolos(ListaID.tipo);
';'
Declaracoes
| ^
Declaracoes → id Se não Simbolos.SimboloRedeclarado(id.nome) então id.simbobj := TSimbolo.Create(id.nome); id.simbobj.TipoVariavel := Declaracoes.TipoVariavel; Símbolos.Instalar(id.simbobj);
ListaID (Matriz | )̂
Simbolos.AtualizarUltimosSimbolos(ListaID.tipo);
';'
Declaracoes
| ^
ListaID → ','
id Se não Simbolos.SimboloRedeclarado(id.nome) então id.simbobj := TSimbolo.Create(id.nome); id.simbobj.TipoVariavel := ListaID.TipoVariavel; Símbolos.Instalar(id.simbobj);
ListaID (Matriz | )̂
Simbolos.AtualizarUltimosSimbolos(ListaID.tipo);
| ':'
Tipo ListaID.tipo := Tipo.tipo;
Quadro 19 - Definição de declarações de variáveis
O objetivo do atributo TipoVarivel (atributo herdado dos não-terminais
Declaracoes e ListaID ) que é atribuído aos símbolos criados é diferenciar as variáveis
(globais, parâmetros, locais ou atributos). Esta diferenciação é necessária na hora da geração
de código.
No Quadro 20 é apresentada a definição dos tipos da linguagem FURBOL.
48
Tipo → 'inteiro' Tipo.tipo := tsInteiro;
| 'logico' Tipo.tipo := tsLogico;
| 'matriz' Tipo.tipo := tsMatriz;
| 'semaforo' Tipo.tipo := tsSemaforo;
| id Simb := Símbolos.ProcurarSimbolo(id.nome); Se Simb.tipo = tsClasse então Tipo.tipo := tsObjeto; Tipo.TAD := Simb;
Matriz → LimitesM Simbolos.AtualizarUltimosSimbolos(LimitesM.tipo)
':'
ListaID Matriz.tipo := ListaID.tipo
LimitesM → '['
Dimensao
']'
| ^
Dimensao → num
Dimensao.liminf := num.valor
'..'
num Dimensao.limsup := num.valor
MaisDimensao
MaisDimensao → ','
Dimensao
| ^
Quadro 20 - Definição dos tipos de dados
O atributo Tipo passou a ser um registro com dois campos. O primeiro identifica o
tipo de símbolo que está sendo declarado. O segundo campo é um ponteiro para um símbolo
na tabela de símbolos e é usado quando uma referência de um TAD está sendo declarada.
Neste caso, o segundo campo do registro aponta para o TAD na tabela de símbolos que está
sendo referenciado.
3.2.3 Procedimentos e tarefas
No Quadro 21 é apresentada a definição de sub-rotinas e a definição de procedimentos.
No Quadro 22 é apresentada a definição da unidade de programa tarefa.
49
EstruturaSubRotinas → 'procedimento'
EstruturaProcedimento EstruturaSubRotinas.codigo := EstruturaProcedimento.codigo; EstruturaSubRotinas.codasm := EstruturaProcedimento.codasm;
| 'tarefa' Se Simbolos.EscopoAtual.Nivel > 0 então erro;
EstruturaTarefa EstruturaSubRotinas.codigo := EstruturaTarefa.codigo; EstruturaSubRotinas.codasm := EstruturaTarefa.codasm;
| ^
EstruturaProcedimento → id Se não Simbolos.SimboloRedeclarado(id.nome) então id.simbobj := TSimbolo.Create(id.nome, tsProcedimento) Simbolos.Instalar(id.simbobj) id.simbobj.TabelaAgregada := Simbolos.AbrirEscopo(id.nome);
ParamFormais
';'
EstruturaDados
EstruturaSubRotinas
CComposto EstruturaProcedimento.codasm := (id.nome || 'proc near' || CRLF || 'push bp' || CRLF || 'mov bp,sp' || CRLF || 'sub sp,' || Simbolos.EscopoAtual.LarguraVariaveis || CRLF || CComposto.codasm || CRLF || 'mov sp,bp' || CRLF || 'pop bp' || CRLF || 'ret ' || Simbolos.EscopoAtual.LarguraParametros || CRLF || id.nome || 'endp' || CRLF || EstruturaSubRotinas.codasm; Simbolos.FecharEscopo;
';'
EstruturaSubRotinas EstruturaProcedimento.codigo := EstruturaProcedimento.codigo || EstruturaSubRotinas.codigo; EstruturaProcedimento.codasm := EstruturaProcedimento.codasm || EstruturaSubRotinas.codasm;
Quadro 21 - Definição de sub-rotinas e procedimentos
EstruturaTarefa → id Se não Simbolos.SimboloRedeclarado(id.nome) então id.simbobj := TSimbolo.Create(id.nome, tsTarefa) Simbolos.Instalar(id.simbobj) id.simbobj.TabelaAgregada := Simbolos.AbrirEscopo(id.nome);
';'
EstruturaDados
EstruturaSubRotinas
CComposto EstruturaTarefa.codasm := (id.nome || 'proc near' || CRLF || 'push bp' || CRLF || 'mov bp,sp' || CRLF || 'sub sp,' || Simbolos.EscopoAtual.LarguraVariaveis || CRLF || CComposto.codasm || CRLF || 'mov sp,bp' || CRLF || 'pop bp' || CRLF || 'ret ' || Simbolos.EscopoAtual.LarguraParametros || CRLF || id.nome || 'endp' || CRLF || EstruturaSubRotinas.codasm; Simbolos.FecharEscopo;
';'
EstruturaSubRotinas EstruturaTarefa.codigo := EstruturaTarefa.codigo || EstruturaSubRotinas.codigo; EstruturaTarefa.codasm := EstruturaTarefa.codasm || EstruturaSubRotinas.codasm;
Quadro 22 - Definição de tarefas
No Quadro 23 é apresentada a definição de parâmetros formais dos procedimentos do
FURBOL.
50
ParamFormais → '('
(ParamValor | ParamRef)
SecaoParam
')'
| ^
SecaoParam → ';'
(ParamValor | ParamRef)
SecaoParam
| ^
ParamValor → id Se não Simbolos.SimboloRedeclarado(id.nome) então id.simbobj := TSimbolo.Create(id.nome); id.simbobj.TipoVariavel := tvValor; Símbolos.Instalar(id.simbobj);
ListaID Simbolos.AtualizarUltimosSimbolos;
ParamRef → 'ref'
id Se não Simbolos.SimboloRedeclarado(id.nome) então id.simbobj := TSimbolo.Create(id.nome); id.simbobj.TipoVariavel := tvReferencia; Símbolos.Instalar(id.simbobj);
ListaID Simbolos.AtualizarUltimosSimbolos;
Quadro 23 - Definição de parâmetros formais
O objetivo do atributo TipoVarivel (atributo herdado dos não-terminais ParamValor
e ParamRef ) que é atribuído aos símbolos criados é diferenciar os dois tipos de parâmetros
(por cópia valor e referência).
3.2.4 Comandos da linguagem
Do Quadro 24 ao Quadro 32 são apresentadas as definições das estruturas dos
comandos da linguagem. Os comandos são especificados nas definições do Quadro 24 e do
Quadro 25. O código para eles é gerado pelas definições nos quadros que vão do Quadro 26
até o Quadro 32.
51
Comando → Identificador
('.' AcessaObjeto | ChamMetodo | ChamProc | L, Atribuição)
Se Identificador.Simb.tipo <> tsObjeto então erro; Comando.codigo := AcessaObjeto.codigo; Comando.codasm := AcessaObjeto.codasm; Se Identificador.Simb.tipo = tsMetodo então Se Identificador.Simb.TipoMetodo <> tmMetodo então erro; Comando.codigo := ChamMetodo.codigo; Comando.codasm := ChamMetodo.codasm; Senão Se Identificador.Simb.tipo = tsProcedimento então Comando.codigo := ChamProc.codigo || GeraCodigoEloEstatico || 'chamada ' || Identificador.Simb.nome; Comando.codasm := ChamProc.codasm || GeraCodigoEloEstatico || 'call ' || Identificador.Simb.nome; Senão Se Simb.tipo = tsTarefa então Comando.codigo := 'disparar(' || Identificador.Simb.nome || ')'; Comando.codasm := 'push ax' || CRLF || 'mov ax,offset ' || Identificador.Simb.nome || CRLF || 'push ax' || CRLF || 'call disparar_proc' || CRLF || 'pop ax' || CRLF; Senão Comando.codigo := Atribuicao.codigo; Comando.codasm := Atribuicao.codasm;
Virgula Comando.codigo := Comando.codigo || Virgula.codigo; Comando.codasm := Comando.codasm || Virgula.codasm;
| CCondicional
Virgula Comando.codigo := CCondicional.codigo || Virgula.codigo; Comando.codasm := CCondicional.codasm || Virgula.codasm;
| CRepeticao
Virgula Comando.codigo := CRepeticao.codigo || Virgula.codigo; Comando.codasm := CRepeticao.codasm || Virgula.codasm;
| CEntrada
Virgula Comando.codigo := CEntrada.codigo || Virgula.codigo; Comando.codasm := CEntrada.codasm || Virgula.codasm;
| CSaida
Virgula Comando.codigo := CSaida.codigo || Virgula.codigo; Comando.codasm := CSaida.codasm || Virgula.codasm;
| CInc
Virgula Comando.codigo := CInc.codigo || Virgula.codigo; Comando.codasm := CInc.codasm || Virgula.codasm;
| CDec
Virgula Comando.codigo := CDec.codigo || Virgula.codigo; Comando.codasm := CDec.codasm || Virgula.codasm;
| 'nl'
Virgula Comando.codigo := 'nl' || Virgula.codigo; Comando.codasm := 'nop' || Virgula.codasm;
Quadro 24 - Definição dos comandos da linguagem
Foram adicionados comandos que permitem a chamada dos métodos definidos nos
TADs. Somente os métodos atualizadores e consultores podem ser invocados diretamente. Os
métodos construtores e destrutores só podem ser chamados através dos comandos criarobj
e destruirobj respectivamente.
52
No Quadro 25 é apresentado a definição para fazer o reconhecimento dos comandos de
criação e exclusão de objetos. As regras semânticas para geração de código destes comandos
são apresentadas mais adiante no Quadro 32.
Comando → 'morre'
Virgula Comando.codigo := 'morre' || Virgula.codigo; Comando.codasm := 'call morre_proc' || Virgula.codasm;
| 'espera'
Virgula Comando.codigo := 'espera' || Virgula.codigo; Comando.codasm := 'call espera_proc' || Virgula.codasm;
| 'repassa'
Virgula Comando.codigo := 'repassa' || Virgula.codigo; Comando.codasm := 'call repassa_proc' || Virgula.codasm;
| 'criarsmf'
CCriarSmf
Virgula Comando.codigo := CCriarSmf.codigo || Virgula.codigo; Comando.codasm := CCriarSmf.codasm || Virgula.codasm;
| 'excluirsmf'
CExcluirSmf
Virgula Comando.codigo := CExcluirSmf.codigo || Virgula.codigo; Comando.codasm := CExcluirSmf.codasm || Virgula.codasm;
| 'p'
CP
Virgula Comando.codigo := CP.codigo || Virgula.codigo; Comando.codasm := CP.codasm || Virgula.codasm;
| 'v'
CV
Virgula Comando.codigo := CV.codigo || Virgula.codigo; Comando.codasm := CV.codasm || Virgula.codasm;
| ‘criarobj’
CCriarObj
Virgula Comando.codigo := CCriarObj.codigo || Virgula.codigo; Comando.codasm := CCriarobj.codasm || Virgula.codasm;
| ‘destruirobj’
CDestruirObj
Virgula Comando.codigo := CDestruirObj.codigo || Virgula.codigo; Comando.codasm := CDestuirobj.codasm || Virgula.codasm;
| Virgula Comando.codigo := Virgula.codigo; Comando.codasm := Virgula.codasm;
Quadro 25 - Definição dos comandos da linguagem (continuação)
Nas produções que reconhecem os comandos criarobj e destruirobj é realizada a
chamada dos procedimentos que implementam os não-terminais CCriarObj e
CDestruirObj , respectivamente. Em seguida é invocado o método que implementa o não-
53
terminal Virgula . Os código gerados são concatenados e atribuídos aos atributos codigo e
codasm do não-terminal Comando.
No Quadro 26 é apresentado a definição dos comandos de atribuição, estrutura de
chamada de procedimento e parâmetros atuais.
Virgula → ';'
Comando Virgula.codigo := Comando.codigo Virgula.codasm := Comando.codasm
| ^
Atribuicao → ':=' Se Atribuicao.deslocamento <> '' então RI := Novo_T; IndCodAsm := 'mov ' || RI || ',' || Ldesloca || CRLF || 'push ' || RI; PreIdAt := 'pop di';
Expressao E.local := Expresssao.local; Se não TiposCompativeis(Expressao.tipo, Atribuicao.tipo) então erro; Se Expressao.deslocamento <> ' ' então Atribuicao.codigo := gerar(idAT'['L.deslocamento']:='E.local); Senão Atribuicao.codigo := gerar(IdAT ':=' E.Local); RX := Registrador; Atribuicao.codasm := IndCodAsm || Expressao.codasm || AtPrelocal || CRLF || 'mov ' || RX || ',' || aTlocal || CRLF || PreIdAT || CRLF || 'mov ' || IdAT ',' RX;
ChamProc → '(' ParN := 0;
ParamAtuais Se ParN <> ChamProc.TabelaSimbolos.QuantidadeParametros então erro; ChamProc.codasm := ParamAtuais.codasm;
')'
| ^ Se 0 <> ChamProc.TabelaSimbolos.QuantidadeParametros então erro;
ParamAtuais → Identificador SimbVar := Identificador.Simb; ParN := ParN + 1; Se ParN > ParamAtuais.SimbProc..QuantidadeParametros então erro; SimbPar := ParamAtuais.SimbProc.TabelaSimbolos.[ParN - 1]; Se SimbVar.tipo <> SimbPar.tipo então erro('tipos incompatíveis'); Se SimbPar.TipoVariavel = tvValor então ParamAtuais.codasm := 'push ' || SimbVar.nome || CRLF || ParamAtuais.codasm; senão ParamAtuais.codasm := 'lea di,' || SimbVar.nome || CRLF || 'push di' || CRLF || ParamAtuais.codasm;
(',' ParamAtuais | ^)
| num ParN := ParN + 1; Se ParN > ParamAtuais.SimbProc..QuantidadeParametros então erro; SimbPar := ParamAtuais.SimbProc.TabelaSimbolos.[ParN - 1]; Se SimbPar.TipoVariavel <> tvValor ou SimbPar.Tipo <> tsInteiro então erro; ParamAtuais.codasm := 'push ' || num.valor || CRLF || ParamAtuais.codasm; (',' ParamAtuais | ^)
Quadro 26 - Definição dos comandos de atribuição, estrutura de chamada de procedimento e parâmetros atuais
Uma consistência na chamada dos procedimentos foi adicionada (Quadro 26). O
compilador passou a verificar se o número de parâmetros reais é o mesmo que o número de
parâmetros formais. Caso exista diferença é emitido um erro de compilação. As ações
semânticas sublinhadas no não-terminal ChamProc verificam se a quantidade de parâmetros é
suficiente. A terceira ação semântica do não-terminal ParamAtuais verifica se a quantidade
54
de parâmetros não excedeu o permitido.
A chamada ao não-terminal Identificador (Quadro 26) permite que a palavra
reservada esse seja usada na chamada de um procedimento.
A segunda produção do não-terminal ParamAtuais (Quadro 26) permite que um valor
inteiro seja passado como parâmetro sem que um identificador seja utilizado.
O Quadro 27 apresenta a definição do comando de repetição e comandos condicionais.
CRepeticao → 'enquanto' CRepeticao.inicio := Novo_L; Expressao.v := Novo_L; Expressao.f := CRepeticao.prox;
Expressao
'faca'
CComposto CComposto.prox := CRpeticao.inicio; CRepeticao.codigo:= CRepeticao.inicio ':' || Expressao.codigo || E.v ':' || CRLF || CComposto.codigo || CRLF || 'goto ' || CRepeticao.inicio; CRepeticao.codasm := CRepeticao.inicio ':" || Expressao.codasm || E.v ':' || CRLF || CComposto.codasm || CRLF || 'jmp' || CRepeticao.inicio;
CCondicional → 'se' Expressao 'entao'
CComposto CComposto1.prox := CCondicional.prox; CCondicional.codigo := Expressao.codigo || E.v ':' || CRLF || CComposto.codigo; CCondicional.codasm := Expressao.codasm || E.v ':' || CRLF || CComposto.codasm;
CCondicional2 CCondicional.codigo := CCondicional.codigo || CCondicional2.codigo; CCondicional.codasm := CCondicional.codasm || CCondicional2.codasm;
CCondicional2 → 'senao'
CComposto fproximo := proximo; CCondicional2.codigo := Expressao.codigo || CRLF || 'goto ' || proximo || CRLF || f ':' || CRLF || CComposto.codigo; CCondicional2.codasm := Expressao.codasm || CRLF || 'jmp ' || proximo || CRLF || f ':' || CRLF || CComposto.codasm;
| ^ CCondicional2.codigo := CCondicional2.codigo || CRLF || f || ':'; CCondicional2.codasm := CCondicional2.codasm || CRLF || f || ':';
Quadro 27 - Definição do comando de repetição e comandos condicionais
No Quadro 28 é apresentado a definição dos comandos de entrada e saída.
55
CEntrada → 'leitura'
'('
id Se Simbolos.ProcurarSimbolo(id.nome) = nil então erro('Identificador não declarado');
LeituraListaID
')' CEntrada.codigo := 'leitura(' || id.nome || ',' || LeituraListaID.codigo;
LeituraListaID → ','
id Se Simbolos.ProcurarSimbolo(id.nome) = nil então erro('Identificador não declarado'); LeituraListaID.codigo := LeituraListaID.codigo || ',' || id.nome;
LeituraListaID
| ^
CSaida → 'imprime'
'('
Expressao
ListaExpressao Se Expressao.tipo = tsinteiro CSaida.codasm := Expressao.Prelocal || CSaida.codasm || CRLF ‘push local’ || CRLF ‘call imprime’;
')' CSaida.codigo := 'imprime(' || Expressao.codigo || ',' || ListaExpressao.codigo ;
ListaExpressao → ','
Expressao ListaExpressao.codigo := ListaExpressao.codigo || Expressao.codigo;
ListaExpressao
| ^
Quadro 28 - Definição dos comandos de entrada e saída
O comando de saída imprime foi extendido para também imprimir expressões do tipo
inteiro. Antes o comando apenas imprimia cadeias de caracteres. As ações semânticas
sublinhadas no não-terminal CSaida verificam se foi enunciada a impressão de uma
expressão do tipo inteiro e em caso positivo gera código para a chamada do procedimento do
núcleo imprime (Quadro 28).
O Quadro 29 apresenta a definição dos comandos incremento e decremento.
56
CInc → '('
id Se Simbolos.ProcurarSimbolo(id.nome) = nil então erro('Identificador não declarado'); Se id.tipo <> tsInteiro então erro('Tipo incopatível'); CInc.codigo := 'inc ' || '(' || id.nome || ')'; CInc.codasm := 'inc ' || id.nome;
')'
CDec → '('
id Se Simbolos.ProcurarSimbolo(id.nome) = nil então erro('Identificador não declarado'); Se id.tipo <> tsInteiro então erro('Tipo incopatível'); CDec.codigo := 'dec ' || '(' || id.nome || ')'; CDec.codasm := 'dec ' || id.nome;
')'
Quadro 29 - Definição dos comandos incremento e decremento
No Quadro 30 é apresentado a definição dos comandos de criação e exclusão de
semáforos.
CCriarSmf → '('
Identificador Se Identificador.Simb.tipo <> tsSemaforo então erro('Tipo incopatível');
','
Expressão Se Expressao.tipo <> tsInteiro então erro;
')' CCriarSmf.codigo := Expressao.codigo || 'criarsmf(' || Identificador.Simb.nome || Expressao.local || ')'; CCriarSmf.codasm := Expressao.codasm || 'push di' || CRLF || 'push cx' || CRLF || 'push ' || Expressao.local || CRLF || 'sub sp,2 || CRLF || 'call criar_smf_proc' || CRLF || 'pop cx' || CRLF || 'mov ' || GeraCodigoAssembly(Identificador.Simb) || ',cl' || CRLF || 'add sp,2' || CRLF || 'pop cx' || CRLF || 'pop di' || CRLF;
CExcluirSmf → '('
Identificador
')' CExcluirSmf.codigo := 'excluirsmf(' || Identificador.Simb.nome || ')'; CExcluirSmf.codasm := 'push di' || CRLF || 'push cx' || CRLF || 'xor cx,cx' || CRLF || 'mov cl,' || GeraCodigoAssembly(Identificador.Simb) || CRLF || 'push cx' || CRLF || 'call excluir_smf_proc' || CRLF || 'pop cx' || CRLF || 'pop di' || CRLF;
Quadro 30 - Definição dos comandos de criação e exclusão de semáforos
As chamadas ao não-terminal Identificador permitem que a palavra reservada
esse , que é reconhecida pelo não-terminal Identificador , seja usada nos comandos de
criação e exclusão de semáforos.
O Quadro 31 apresenta a definição dos comandos de operação P e V em semáforos.
57
CP → '('
Identificador
')' CP.codigo := 'p(' || Identificador.Simb.nome || ')'; CP.codasm := 'push di' || CRLF || 'push cx' || CRLF || 'xor cx,cx' || CRLF || 'mov cl,' || GeraCodigoAssembly(Identificador.Simb) || CRLF || 'push cx' || CRLF || 'call p_smf_proc' || CRLF || 'pop cx' || CRLF || 'pop di' || CRLF;
CV → '('
Identificador
')' CV.codigo := 'v(' || Identificador.Simb.nome || ')'; CV.codasm := 'push di' || CRLF || 'push cx' || CRLF || 'xor cx,cx' || CRLF || 'mov cl,' || GeraCodigoAssembly(Identificador.Simb) || CRLF || 'push cx' || CRLF || 'call v_smf_proc' || CRLF || 'pop cx' || CRLF || 'pop di' || CRLF;
Quadro 31 - Definição dos comandos de operação P e V em semáforos
As chamadas ao não-terminal Identificador permitem que a palavra reservada esse
seja usada nos comandos P e V.
No Quadro 32 são apresentadas as ações semânticas para a geração de código dos
comandos de criação e exclusão de objetos.
O comando criarobj aceita como primeiro parâmetro uma variável do tipo objeto.
Aceita também, opcionalmente como segundo parâmetro a chamada de um método do TAD
do qual o primeiro parâmetro é uma referência. Esse método deve ser obrigatoriamente do
tipo construtor e é invocado logo após que a memória para objeto é alocada. O endereço da
memória alocada é atribuído a variável especificada no primeiro parâmetro.
O comando destruirobj é semelhante ao comando criarobj . A diferença é que o
método chamado deve ser do tipo destrutor. Esse método é invocado antes que a memória
alocada para o objeto (que é o conteúdo do primeiro parâmetro do comando) seja liberada.
58
CCriarObj → '('
Identificador Se Identificador.Simb.Tipo <> tsObjeto então erro;
ChamConstrutor
')'
‘;’ CCriarObj.codigo := ‘criarobj(’ || Identificador.Simb.nome || ‘)’ || CRLF || ChamConstrutor.codigo; CriarObj.codasm := ‘push ax’ || CRLF || ‘push’ || Identificador.Simb.TamanhoAtributos || CRLF || ‘call criarobj’ || CRLF || ‘mov’ || GeraCodigoAssembly(Identificador.Simb) || ‘, ax’ || CRLF || ‘pop ax’ || CRLF || ChamConstrutor.codasm;
CDestruirObj → '('
Identificador Se Identificador.Simb.Tipo <> tsObjeto então erro;
ChamDestrutor
')'
‘;’ CDestruirObj.codigo := ChamDestrutor.codigo || ‘destruirobj(’ || Identificador.Simb.nome || ‘)’ || CRLF; CDestruirObj.codasm := ChamDestrutor.codasm || ‘push ax’ || CRLF || ‘mov ax, ’ || GeraCodigoAssembly(Identificador.Simb) || ‘push ax’ || CRLF || ‘call destruirobj’ || CRLF || ‘pop ax’ || CRLF;
Quadro 32 - Definição dos comandos de criação e exclusão de objetos
As chamadas ao não-terminal Identificador permitem que a palavra reservada esse
seja usada nos comandos de criação e exclusão de objetos.
As ações do não-terminal CCriarObj verificam se a variável usada como primeiro
parâmetro do comando criarobj é do tipo objeto. Em seguida é feita a chamada ao não-
terminal ChamConstrutor que se encarrega de gerar código para a chamada do método
construtor caso essa chamada seja enunciada pelo programador. Após a chamada ao não-
terminal ChamConstrutor é gerado código para a chamada do procedimento do núcleo
criarobj e para a atribuição do resultado do procedimento a variável do tipo objeto. Ao final
o código gerado pelas ações do não-terminal CCriarObj é concatenado ao código gerado
pela chamada ao não-terminal ChamConstrutor .
As ações do não-terminal CDestruirObj são semelhantes às ações do não-terminal
CCriarObj . A diferença é que neste caso são realizadas chamadas ao não-terminal
ChamDestrutor e ao procedimento do núcleo destruirobj . Outra diferença é que no final o
código gerado é concatenado ao contrário. Primeiro é realizado a chamada ao método
destrutor e depois a chamada ao procedimento do núcleo que exclui o objeto.
59
3.2.5 Expressões
A estrutura de controle de expressões pode ser vista do Quadro 33 até o Quadro 38.
Esta definição foi construída a partir da definição de precedência de operadores definida em
Aho, Sethi e Ullman (1995).
Expressao → Expressao2 Relacao.v := Expressao2.v; Relacao.f := Expressao2.f; Relacao.local := Expressao2.local; Relacao.codigo := Expressao2.codigo; Relacao.codasm := Expressao2.codasm;
Relacao Expressao.local := Relacao.local; Expressao.codigo := Relacao.codigo; Expressao.codasm := Relacao.codasm; Expressao.v := Relacao.v; Expressao.f := Relacao.f;
Expressao2 → TC ELi.v := TC.v; ELi.f := TC.f; ELi.local := TC.local; ELi.codigo := TC.codigo; ELi.codasm := TC.codasm;
EL Expressao2.local := ELs.local; Expressao2.codigo := ELs.codigo; Expressao2.codasm := ELs.codasm;
Quadro 33 - Definição da estrutura de controle de expressões
Relacao → '=' Relacao.codigo := 'se ' || Relacao.local || ' = ' || Expressao2.local || 'goto ' || E.v || 'goto ' || E.f; Relacao.codasm := 'mov ' || R0 || ',' || Relacao.local || CRLF || 'cmp ' || R0 || ',' || local || CRLF || 'je ' || Rv || CRLF || 'jmp ' || Rf;
| '<>' Relacao.codigo := 'se ' || Relacao.local ' <> ' Expressao2.local || 'goto' || E.v || 'goto' || E.f; Relacao.codasm := 'mov ' || R0 || ',' || Relacao.local || CRLF || 'cmp ' || R0 || ',' || local || CRLF || 'jne ' || Rv || CRLF || 'jmp ' || Rf;
| '<' Relacao.codigo := 'se ' || Relacao.local || ' < ' || Expressao2.local || 'goto ' || E.v || 'goto ' || E.f; Relacao.codasm := 'mov ' || R0 || ',' || Relacao.local || CRLF || 'cmp ' || R0 || ',' || local || CRLF || 'jb ' || Rv || CRLF || 'jmp ' || Rf;
| '>' Relacao.codigo := 'se ' || Relacao.local || ' > ' || Expressao2.local || 'goto ' || E.v || 'goto ' || E.f; Relacao.codasm := 'mov ' || R0 || ',' || Relacao.local || CRLF || 'cmp ' || R0 || ',' || local || CRLF || 'ja ' || Rv || CRLF || 'jmp ' || Rf;
^
Quadro 34 - Definição da estrutura de controle de expressões (continuação)
60
EL → '+'
TC EL1i.local := Novo_T; EL1i.codigo := EL.codigo || TC.codigo || CRLF || EL1i.local || ':=' || EL.local || '+' || TC.local; EL1i.codasm := EL.codigo || TC.codigo || CRLF || 'mov ' || RX || ',' || EL.local || CRLF || 'add ' || RX || ',' || Tc.local || CRLF || 'mov ' || EL1i.local || ',' || RX;
EL1 EL.local := EL1s.local; EL.codigo := EL1s.codigo; EL.codasm := EL1s.codasm;
| '-'
TC EL1i.local := Novo_T; EL1i.codigo := EL.codigo || TC.codigo || CRLF || EL1i.local || ':=' || EL.local || '-' || TC.local; EL1i.codasm := EL.codigo || TC.codigo || CRLF || 'mov ' || RX || ',' || EL.local || CRLF || 'sub ' || RX || ',' || Tc.local || CRLF || 'mov ' || EL1i.local || ',' || RX;
EL1 EL.local := EL1s.local; EL.codigo := EL1s.codigo; EL.codasm := EL1s.codasm;
| ^ EL.v := ELs.v; EL.f := ELs.f; EL.local := ELs.local; EL.codigo := ELs.codigo; EL.codasm := ELs.codasm;
TC → F TL1.v := F.v; TL1.f := F.f; TL1.local := F.local; TL1.codigo := F.codigo; TL1.codasm := F.codasm;
TL T.v := TLs.v; T.f := TLs.f; TC.local := TLs.local; TC.codasm := TLs.codasm; TC.codigo := TLs.codigo;
Quadro 35 - Definição da estrutura de controle de expressões (continuação)
61
TL → '*'
F TL1.local := Novo_T; TL1.codigo := TL.codigo || F.codigo || CRLF || TL1.local ':=' TL.local || '*' || F.local; TL1.codasm : = TL.codigo || F.codigo || CRLF || 'mov ax,' || TLLOCAL || CRLF || 'mul ' || Local || CRLF || 'mov ' || TliLocal || ',ax';
TL1 TL.local := TL1s.local; TL.codigo := TL1s.codigo; TL.codasm := TL1s.codasm;
| '/'
F TL1.local := Novo_T; TL1.codigo := TL.codigo || F.codigo || CRLF || TL1.local ':=' TL.local || '/' || F.local; TL1.codasm : = TL.codigo || F.codigo || CRLF || 'mov ax, ' || TLLOCAL || CRLF || 'div ' || Local || CRLF || 'mov ' || TliLocal || ',ax' || CRLF ||;
TL1 TL.local := TL1s.local; TL.codigo := TL1s.codigo; TL.codasm := TL1s.codasm;
| 'E'
F TL.v := Novo_L; TL.f := TL1.f; F.v := TL1.v; F.f := TL1.f; TL1.codigo := TL.codigo || TL1.v ':' || F.codigo; TL1.codasm := TL.codasm || TL1.v ':' || F.codasm;
TL1 TL.v := TL1s.v; TL.f := TL1s.f; TL.codasm := TL1s.codasm; TL.codigo := TL1s.codigo;
| ^ TL.v := TLs.v; TL.f := TLs.f; TL.local := TLs.local; TL.codigo := TLs.codigo; TL.codasm := TLs.codasm;
Quadro 36 - Definição da estrutura de controle de expressões (continuação)
62
F → '(' Expressao.v := F.v; Expressao.f := F.f;
Expressao
')' F.local := Expressao.local; F.codigo := Expressao.codigo; F.codasm := Expressao.codasm;
| '-'
Expressao F.local := Novo_T; F.codigo := Expressao.codigo || CRLF || gerar(F.local || ':=' || ' uminus ' || E.local);
| 'nao' Expressao.v := F.f; Expressao.f := F.v;
Expressao
| Identificador L Se L.deslocamento := 0 então F.local := L.local; senão F.local := Novo_T; gerar(F.local || ':=' || L.local || '[' || L.deslocamento || ']');
| num F.local := num.valor; F.codigo := ''; F.codasm := '';
| ‘nulo’ F.local := ‘0’ ; F.codigo := ''; F.codasm := '';
| ^
L → Lista_E ']'
L.local := Novo_T; L.deslocamento := Novo_T; L.codigo := gerar(L.local := c(Lista_E.array); L.codigo := gerar(L.deslocamento ':=' Lista_E.local '*' Largura(Lista_E.array)); L.codasm := Lista_E.codasm || CRLF || 'mov dx,' || Lista_E.local || CRLF || 'mov al,' || VerificaTipoMatriz(Lista_E.array) || CRLF || 'mul dx' || CRLF || 'mov ' || L.deslocamento || ',ax' || CRLF || 'add ' || L.deslocamento || ',' || Lc || CRLF || 'add ' || L.deslocamento || ',2';
| id L.local := L.Simb.local; L.deslocamento := '';
Quadro 37 - Definição da estrutura de controle de expressões (continuação)
A chamada ao não-terminal Identificador realizada no não-terminal F permite que
a palavra reservada esse possa ser usada em expressões.
A quinta produção do não-terminal F (Quadro 37) faz o reconhecimento da palavra
reservada nulo que para o compilador é equivalente ao valor 0. Essa palavra é compatível
com qualquer variável que seja uma referência a um TAD, podendo ser atribuída e comparada
a essas variáveis. Seu objetivo é permitir que os usuários possuam uma forma de indicar que
uma referência a um TAD é nula (não está apontando para uma área de memória previamente
alocada para o objeto).
O atributo do não-terminal L Simb agora é sintetizado pelo não-terminal
Identificador (o qual permite que um identificador seja acessado através da palavra esse ),
que precede L nos não-terminais Comando (Quadro 24) e F (Quadro 37). Antes a produção
de L analisava o token atual e sintetizava o atributo Simb . Agora, como a produção do não-
63
terminal Identificador avança a analise léxica e altera o token atual, tornou-se necessário
que a própria produção de Identificador sintetizasse o atributo Simb , passando- o para L
como atributo herdado.
Lista_E → id
'['
Expressao Ri.matriz := id.local; Ri.local := Expressao.local; Ri.ndim := 1;
R Lista_E.matriz := Rs.matriz; Lista_E.local := Rs.local; Lista_E.ndim := Rs.ndim;
R → Expressao t := Novo_T; m := Ri.ndim + 1; gerar(t || ':=' || Lista_E.local) limite(Lista_E.matriz,m); R1i.matriz := Ri.matriz; R1i.local := t; R1i.ndim := m; R1i.codasm := Expressao.codasm || CRLF || 'mov cx,' || local || CRLF || 'mov al,' || limite(Lista_E.array,m) || CRLF || 'mul cx' || CRLF || 'mov ' || t || ',ax' || CRLF || 'add ' || t || ',' || Elocal;
R1 Rs.matriz := R1s.matriz; Rs.local := R1s.local; Rs.ndim := R1s.ndim; Rs.codasm := R1s.codasm;
| ^ Rs.matriz := Ri.matriz; Rs.local := Ri.local; Rs.ndim := Ri.ndim; Rs.codasm := Ri.codasm;
Quadro 38 - Definição da estrutura de controle de expressões (continuação)
3.2.6 Definições dos TADs
No Quadro 39 é apresentada a especificação dos TADs definidos pelos usuários.
64
DefinicoesTADs → ‘classe’
TAD
DefinicoesTADs1 DefinicoesTADs.codigo := TAD.codigo || DefinicoesTADs1.codigo; DefinicoesTADs.codasm := TAD.codasm || DefinicoesTADs1.codasm;
| ^
TAD → id Se não Simbolos.SimboloRedeclarado(id.nome) então id.simbobj := TSimbolo.Create(id.nome, tsClasse); Simbolos.Instalar(id.simbobj); id.simbobj.TabelaMembros := Símbolos.AbrirEscopo(id.nome);
AtributosTAD
MetodosTAD
‘fim’ TAD.codigo := MetodosTAD.codigo; TAD.codasm := MetodosTAD.codasm; Simbolos.FecharEscopo;
';'
AtributosTAD → id Se não Símbolos.SimboloRedeclarado(id.nome) então id.simbobj := TSimbolo.Create(id.nome); id.simbobj.TipoVariavel := tvAtributo; Símbolos.Instalar(id.simbobj);
ListaID (Matriz | )̂
Simbolos.AtualizarUltimosSimbolos(ListaID.tipo);
‘;’
AtributosTAD
| ^
MetodosTAD → (‘procedimento’ | ‘construtor’ | ‘destrutor’)
MetodoTAD
MetodosTAD1 MetodosTAD.codigo := MetodoTAD.codigo || MetodosTAD1.codigo; MetodosTAD.codasm := MetodoTAD.codasm || MetodosTAD1.codasm;
| ^
Quadro 39 - Especificação dos TADs definidos pelos usuários
Um TAD é composto por atributos (membros de dados) e métodos (funções-
membros). Ambos são opcionais. Os atributos devem ser declarados obrigatoriamente antes
dos métodos.
Um atributo pode ser um tipo primitivo, um array finito ou uma referência para outro
TAD previamente definido. Arrays dinâmicos não são permitidos como atributos e um erro de
compilação é emitido caso sejam declarados como tal.
Um método é classificado como construtor quando é definido usando-se a palavra
reservada construtor . Para os métodos destrutores usa-se a palavra reservada destrutor e
para os métodos consultores e atualizadores a palavra procedimento .
No Quadro 40 é apresentada a especificação de um método definido pelos usuários.
65
MetodoTAD → id Se não Simbolos.SimboloRedeclarado(id.nome) então id.simbobj := TSimbolo.Create(id.nome, tsMetodo); Simbolos.Instalar(id.simbobj); id.simbobj.TabelaAgregada := Símbolos.AbrirEscopo(id.nome); id.simbobj.TipoMetodo := MetodoTAD.TipoMetodo; ParamOculto := TSimbolo.Create(‘esse’, tsObjeto); Símbolos.Instalar(ParamOculto);
ParamFormais
‘;’
EstruturaDados
CComposto MetodoTAD.codigo := id.nome || ‘proc near’ || CRLF || CComposto.codigo || CRLF || ‘ret’ || CRLF || id.nome || ‘endp’ ; MetodoTAD.codasm := id.nome || ‘proc near’ || CRLF || ‘push bp’ || CRLF || ‘mov bp, sp’ || CRLF || ‘sub sp, ’ || Símbolos.EscopoAtual.LarguraVariaveis || CRLF || CComposto.codasm || ‘mov sp, bp’ || CRLF || ‘pop bp’ || CRLF || ‘ret’ || Símbolos.EscopoAtual.LarguraParametros + 2 || CRLF || id.nome || ‘endp’; Símbolos.FecharEscopo;
';'
Quadro 40 - Especificação de um método
A definição de um método é semelhante à definição de um procedimento. Contudo
existem algumas diferenças, as quais são:
a) métodos não podem ser aninhados;
b) o código de um método pode utilizar a palavra reservada esse que é usada para
acessar explicitamente um membro do TAD ao qual o método pertence ou quando
uma referência para esse TAD é esperada.
Quando um método é definido, o compilador instala na lista de símbolos do método
um parâmetro oculto (não definido pelo programador) chamado de esse . Este parâmetro é
sempre o primeiro da lista e é do tipo objeto. Ele é uma referência (ponteiro) para o local onde
encontram-se os atributos do TAD e será usado internamente pelo compilador para acessar
esses atributos.
No Quadro 41 é apresentada a especificação do acesso aos membros dos TADs.
66
Identificador → ‘esse’ SimbMetodo := Símbolos.ProcurarSimboloNivel(Símbolos.EscopoAtual.Nome, Símbolos.EscopoAtual.Nivel-1); Se SimbMetodo = nil ou SimbMetodo.TipoMetodo = tmNenhum então erro;
Esse Identificador.Simb := Esse.Simb;
| id Identificador.Simb := Símbolos.ProcurarSimbolol(id.nome); Se Identificador.Simb = nil então erro;
Esse → ‘.’
id Esse.Simb := Símbolos.ProcurarSimboloNivel(id.nome, Símbolos.EscopoAtual.Nivel-1); Se Esse.Simb = nil então erro;
| ^ Esse.Simb := Símbolos.ProcurarSimbolo (‘esse’); Se Esse.Simb = nil então erro;
AcessaObjeto → id Simb := AcessaObjeto.Objeto.TAD.TabelaMembros.ProcurarSimbolo(id.nome); Se Simb = nil então erro; Se Simb.TipoMetodo <> tmMetodo então erro;
ChamMetodo AcessaObjeto.codigo := ChamMetodo.codigo; AcessaObjeto.codasm := ChamMetodo.codasm;
ChamMetodo → ChamMetodo2 ChamMetodo.codigo := ‘chamada ’ || ChamMetodo.Metodo.Nome; ChamMetodo.codasm := ‘push’ || GeraCodigoAssembly(ChamMetodo.Objeto) || CRLF || ‘push bp’ || CRLF || ‘call ’ ChamMetodo.Metodo.Nome; ChamMetodo.codigo := ChamMetodo2.codigo || ChamMetodo.codigo; ChamMetodo.codasm := ChamMetodo2.codasm || ChamMetodo.codigo;
ChamMetodo2 → ‘(‘ ParN := 1;
ParamFormais Se ParN <> ChamMetodo2.Metodo.TabelaAgregada.QuantidadeParametros então erro; ChamMetodo2.codigo := ParamAtuais.codigo; ChamMetodo2.codasm := ParamAtuais.codasm;
‘)’
| ^ Se 1 <> ChamMetodo2.Metodo.TabelaAgregada.QuantidadeParametros então erro;
ChamConstrutor → ','
id Simb := ChamConstrutor.Objeto.TAD.TabelaMembros.ProcurarSimbolo(id.nome); Se Simb = nil então erro; Se Simb.TipoMetodo <> tmConstrutor então erro;
ChamMetodo ChamConstrutor.codigo := ChamMetodo.codigo; ChamConstrutor.codasm := ChamMetodo.codasm;
| ^
ChamDestrutor → ','
id Simb := ChamDestrutor.Objeto.TAD.TabelaMembros.ProcurarSimbolo(id.nome); Se Simb = nil então erro; Se Simb.TipoMetodo <> tmDestrutor então erro;
ChamMetodo ChamDestrutor.codigo := ChamMetodo.codigo; ChamDestrutor.codasm := ChamMetodo.codasm;
| ^
Quadro 41 - Especificação do acesso aos membros dos TADs
Como já foi informado, a palavra reservada esse só pode ser usada dentro de um
método. Quando ela é usada seguida de um ponto e de um identificador, este identificador é
procurado pelo gerenciador de símbolos somente um nível acima do nível do método, ou seja,
o identificador deve ser um membro do TAD do qual o método pertence. Caso a palavra esse
não seja usada seguida de um ponto, o parâmetro oculto é localizado, pois esse é seu nome.
67
Isso pode ser necessário quando uma referência para o objeto atual é esperada (Quadro 42).
01)classe TPonto 02) x, y: inteiro; 03) 04) (...) 05) 06) procedimento AutoDestruir; 07) inicio 08) destruirobj(esse); 09) fim; 10)fim;
Quadro 42 - Exemplo do uso do parâmetro oculto esse
Na linha 8 do exemplo apresentado, a palavra esse é usada sem um ponto em seguida.
Neste caso o compilador localizará o parâmetro oculto que é uma referência do objeto atual e
seu conteúdo será o parâmetro do procedimento do núcleo destruirobj . Como resultado
deste enunciado a memória alocada para o objeto será liberada.
Somente os métodos consultores e atualizadores de um TAD podem ser acessados
diretamente. Os atributos só podem ser acessados pelo código de um método e os métodos
construtores e destrutores pelos comandos criarobj e destruirobj respectivamente.
A chamada de um método é semelhante à chamada de um procedimento. A única
diferença é que depois que os parâmetros informados pelo programador são empilhados o
compilador também empilha o conteúdo da variável do tipo objeto que está sendo
referenciada (parâmetro esse do método) . Caso um método invoque outro método do
mesmo objeto o parâmetro esse é propagado.
3.2.7 Exemplo de um TAD definido conforme a nova especificação do FURBOL.
No Quadro 43 é apresentado o exemplo de um TAD definido, conforme a nova
especificação da linguagem FURBOL implementada neste trabalho.
68
classe TPilha ftamanho: inteiro; ftopo: inteiro; fpilha: matriz[1..7]: inteiro; construtor constroi; inicio ftamanho := 7; ftopo := 0; fim; procedimento empilha(i: inteiro); inicio se (ftopo = ftamanho) entao inicio imprime("A pilha esta cheia"); fim senao inicio ftopo := ftopo + 1; fpilha[ftopo] := i; fim; fim; procedimento desempilha; inicio se (ftopo = 0) entao inicio imprime("A pilha esta vazia"); fim senao inicio ftopo := ftopo - 1; fim; fim; procedimento topo; inicio se (ftopo = 0) entao inicio imprime("A pilha esta vazia"); fim senao inicio imprime(fpilha[ftopo]); fim; fim; procedimento vazio; inicio se (ftopo = 0) entao inicio imprime("A pilha esta vazia"); fim senao inicio imprime("A pilha nao esta vazia"); fim; fim; procedimento listar; var i: inteiro; inicio imprime("Valores da pilha:"); i := ftopo; enquanto (i > 0) faca inicio imprime(fpilha[i]); i := i - 1; fim; fim; fim;
Quadro 43 - Exemplo de um TAD definido conforme a nova especificação da linguagem FURBOL
69
No exemplo apresentado TPilha é o nome do TAD definido. Ftamanho , ftopo e
fpilha são os atributos do TAD. constroi é o método construtor. empilha e desempilha
são métodos atualizadores e topo , vazio e listar métodos consultores.
No Quadro 44 é apresentado o exemplo de um programa que cria uma instância do
TAD definido no Quadro 43 e usa essa instância.
programa pilha; classe TPilha (...) fim; var gi, gj: inteiro; gpilha: TPilha; inicio criarobj(gpilha, constroi); gi := 0; enquanto (gi < 10) faca inicio gi := gi + 1; gpilha.empilha(gi); fim; gpilha.listar; destruirobj(gpilha); fim.
Quadro 44 - Exemplo de um programa que usa o TAD definido no Quadro 43
3.2.8 Exemplo de uma tabela de símbolos para um programa que define e usa um TAD
O Quadro 45 apresenta um programa que define e usa um TAD.
70
programa Retangulo; classe TRetangulo esquerda, topo, direita, fundo: inteiro; construtor constroi(esquerda, topo, direita, fun do: inteiro); inicio imprime("Construindo um retangulo"); esse.esquerda := esquerda; esse.topo := topo; esse.direita := direita; esse.fundo := fundo; fim; procedimento Area; var largura, altura, area: inteiro; inicio imprime("Area do Retangulo:"); largura := direita - esquerda; altura := fundo - topo; area := largura * altura; imprime(area); fim; destrutor destroi; inicio imprime("Destruindo um retangulo"); fim; fim; var gRet: TRetangulo; inicio criarobj(gRet, constroi(0, 0, 20, 20)); gRet.Area; destruirobj(gRet, destroi); fim.
Quadro 45 - Exemplo de um programa que define e usa um TAD
A Figura 3 apresenta o esboço da tabela de símbolos para o programa do Quadro 45.
71
Escopo: TRetangulo Nível: 1 Símbolo: esquerda Tipo: tsInteiro TipoVariavel: tvAtributo Símbolo: topo Tipo: tsInteiro TipoVariavel: tvAtributo Símbolo: direita Tipo: tsInteiro TipoVariavel: tvAtributo Símbolo: fundo Tipo: tsInteiro TipoVariavel: tvAtributo Símbolo: constroi Tipo: tsMetodo TipoMetodo: tmConstrutor TabelaAgregada Símbolo: Area Tipo: tsMetodo TipoMetodo: tmMetodo TabelaAgregada Símbolo: destroi Tipo: tsMetodo TipoMetodo: tmDestrutor TabelaAgregada
Escopo: constroi Nível: 2 Símbolo: esse Tipo: tsObjeto (TRetangulo) TipoVariavel: tvValor Símbolo: esquerda Tipo: tsInteiro TipoVariavel: tvValor Símbolo: topo Tipo: tsInteiro TipoVariavel: tvValor Símbolo: direita Tipo: tsInteiro TipoVariavel: tvValor Símbolo: fundo Tipo: tsInteiro TipoVariavel: tvValor
Escopo: Principal Nível: 0 Símbolo: TRetangulo Tipo: tsClasse TabelaMembros Símbolo: gRet Tipo: tsObjeto (TRetangulo) TipoVariavel: tvGlobal
Escopo: Area Nível: 2 Símbolo: esse Tipo: tsObjeto (TRetangulo) TipoVariavel: tvValor Símbolo: largura Tipo: tsInteiro TipoVariavel: tvLocal Símbolo: altura Tipo: tsInteiro TipoVariavel: tvLocal Símbolo: area Tipo: tsInteiro TipoVariavel: tvLocal
Escopo: destroi Nível: 2 Símbolo: esse (TRetangulo) Tipo: tsObjeto TipoVariavel: tvValor
Figura 3 - Esboço da tabela de símbolos para o programa do Quadro 45
72
3.3 ASPECTOS RELEVANTES CONSIDERADOS PARA IMPLEMENTAÇÃO DA EXTENSÃO DA LINGUAGEM PROPOSTA
Nessa seção são apresentados os aspectos relevantes considerados para implementação
da extensão da linguagem proposta.
3.3.1 Criação de objetos
O conteúdo de uma variável do tipo objeto é apenas uma referência (ponteiro) para o
segmento onde encontra-se a área de dados destinada aos atributos do objeto. A criação do
objeto (alocação de memória para os dados) é feita de forma explícita através do comando
criarobj . Uma variável do tipo objeto ocupa dois bytes, conforme pode ser observado no
Quadro 46.
Furbol assembly classe TPonto x, y: inteiro; construtor constroi(x, y: inteiro); inicio esse.x := x; esse.y := y; fim; fim; var ponto1: TPonto; ponto2: TPonto;
ponto1 dw ? ponto2 dw ?
Quadro 46 - Exemplo de variáveis do tipo objeto
As variáveis ponto1 e ponto2 são duas variáveis globais. São referências para o
TAD TPonto . Em assembly dw é uma diretiva para o montador alocar dois bytes para uma
variável (endereço de memória).
No Quadro 47 é apresentada a criação de um objeto do TAD TPonto referenciado pela
variável ponto1 que foi apresentado no Quadro 46.
Furbol assembly criarobj(ponto1);
1) push ax 2) push 4 3) call criarobj 4) mov ponto1,ax 5) pop ax
Quadro 47 - Exemplo da criação de um objeto
No Quadro 47 pode-se observar que o segundo parâmetro do comando criarobj
(chamada de um construtor) é opcional. A linha 1 em assembly salva o conteúdo do
registrador AX na pilha, pois o mesmo será usado a seguir. A linha 2 empilha o valor 4 na
73
pilha, que será o parâmetro do procedimento criarobj , que espera um valor inteiro
indicando o tamanho da representação do TAD (atributos) em bytes. O compilador consulta a
tabela de símbolos agregada ao TAD TPonto para saber que o mesmo possui tamanho 4
(atributos x e y , ambos com tamanho 2). A linha 3 invoca o procedimento criarobj . A linha
4 atribui o resultado do procedimento criarobj que está no registrador AX a variável global
ponto1 . Após esse comando o conteúdo da variável ponto1 será o endereço do segmento
alocado para os atributos do objeto, que é o retorno do procedimento criarobj . A linha 5
restaura o conteúdo do registrador AX que foi salvo na linha 1.
No Quadro 48 é apresentada a criação de mais um objeto do TAD TPonto apresentado
no Quadro 46. Desta vez a variável usada será ponto2 e o método construtor constroi será
invocado. Quando no comando criarobj a chamada de um método construtor é enunciada,
primeiro é realizada a criação do objeto (linhas 1, 2, 3, 4 e 5 em assembly) e em seguida é
feita a chamada do construtor (linhas 6, 7, 8, 9 e 10 em assembly). Nas linhas 6 e 7 são
empilhados os parâmetros gj e gi respectivamente. Na linha 8 é empilhado o parâmetro
ponto2 (parâmetro oculto). Na linha 9 o registrador BP é empilhado. Na linha 10 o método
construtor é invocado. gi e gj são variáveis globais do tipo inteiro e parâmetros do
construtor. A chamada de um método construtor funciona da mesma forma que a chamada de
um método comum. Os aspectos relacionados a chamada de um método são apresentados
adiante, na seção 3.3.3.
Furbol assembly criarobj(ponto2, constroi(gi, gj)); 01) push ax
02) push 4 03) call criarobj 04) mov ponto2,ax 05) pop ax 06) push gj 07) push gi 08) push ponto2 09) push bp 10) call TPonto@constroi
Quadro 48 - Exemplo da criação de um objeto com chamada de método construtor
O registrador BP só é empilhado na linha 8 para manter um padrão entre as chamadas
de procedimentos e métodos no FURBOL que é possuir sempre a mesma quantidade de bytes
na pilha entre os parâmetros e as variáveis locais. O valor do registrador BP empilhado não é
utilizado pelo método chamado e só seria necessário caso fosse possível a definição de
métodos aninhados. Neste caso ele serviria como elo de acesso.
74
3.3.1.1 Procedimento criarobj
O procedimento criarobj que é invocado pelo comando de mesmo nome é um
procedimento criado e adicionado aos outros procedimentos do núcleo do FURBOL.
O procedimento recebe como parâmetro o tamanho do TAD em bytes, aloca memória
na heap para o objeto e retorna no registrador AX o valor do segmento do bloco alocado para
o objeto. Para alocar memória é usada a função 48h (Quadro 49) do SO MicroSoft – Disk
Operating System (MS-DOS).
Entradas: AH = 48h BX = número de parágrafos (unidades de 16 bytes) que se deseja alocar Saída: CF = indica a ocorrência de erro (0 = ok) AX = contém o código do erro ou o valor do segmento do bloco alocado BX = em caso de erro por falta de memória, contém o tamanho máximo que pode ser alocado Fonte: adaptado de Santos e Raymundi Júnior (1989, p. 244).
Quadro 49 – Uso da função 48h do MS-DOS
O Quadro 50 apresenta o trecho do procedimento no qual a memória é alocada. O
código completo da rotina pode ser encontrado no Apêndice A.
1) mov bx, ax ; bx <- quantidade de parágrafos 2) mov ah, 48h ;função do DOS 3) int 21h
Quadro 50 - Trecho do procedimento criarobj
Na linha 1 é atribuído ao registrador BX o conteúdo do registrador AX que possui a
quantidade de parágrafos de memória que devem ser alocados para o objeto. Essa quantidade
foi previamente calculada pela rotina. A linha 2 atribui ao registrador AH o código da função
do MS-DOS que será invocada. A linha 3 gera a interrupção que transfere a execução para o
MS-DOS. Caso a alocação de memória falhe uma mensagem de erro é emitida e o registrador
AX é zerado.
3.3.2 Parâmetro oculto esse
Todo método possui um parâmetro oculto chamado esse . O conteúdo do parâmetro
esse é o endereço do segmento alocado para os atributos do objeto. O parâmetro oculto
esse é necessário, pois esta foi a solução criada para que o método chamado acesse os
atributos do objeto.
75
O parâmetro esse é sempre o primeiro parâmetro de um método, portanto sua posição
na pilha é sempre a mesma e não é necessário nenhum cálculo por parte do compilador para
acessar o seu conteúdo. O seu conteúdo sempre será a palavra apontada pelo registrador BP
mais um deslocamento de 6 bytes (word ptr [bp+6] em assembly). Os 6 bytes que estão na
pilha entre o byte apontado por BP e o parâmetro esse são apresentados no exemplo do
Quadro 51.
Aumento da pilha
BP Exemplo de endereço
Conteúdo da pilha
Comentário
BP � 992 conteúdo salvo do registrador BP
O primeiro comando de um procedimento é salvar o conteúdo do registrador BP, pois o mesmo será modificado. O segundo é atribuir ao registrador BP o endereço do topo da pilha (neste exemplo 992).
994 endereço de retorno do procedimento
Endereço de retorno que é empilhado implicitamente pela instrução CALL (2 bytes).
996 elo de acesso Ponteiro usado para acessar as variáveis não-locais nos procedimentos aninhados (2 bytes).
998 parâmetro esse Parâmetro oculto esse (2 bytes).
1000 parâmetros normais
Parâmetros definidos pelo usuário.
Quadro 51 – Localização do parâmetro esse na pilha
3.3.3 Chamada de métodos
A chamada de um método é semelhante à chamada de um procedimento. Primeiro os
parâmetros definidos pelo programador são empilhados da direita para a esquerda. Em
seguida o conteúdo da variável do tipo objeto que está sendo referenciada também é
empilhado (parâmetro oculto esse) . Por ultimo é empilhado o registrador BP do
procedimento chamador e é feita a chamada do método.
Para o correto funcionamento do programa, no momento da chamada de um método, o
conteúdo da variável do tipo objeto que está sendo referenciada deve ser o endereço
(ponteiro) do segmento alocado para os atributos do objeto, ou seja, o objeto deve ter sido
previamente criado através do comando criarobj .
No Quadro 52 é apresentado um exemplo de um programa em FURBOL que faz a
76
chamada de um método.
01)programa teste; 02) 03)classe TPonto 04) x, y: inteiro; 05) procedimento setXY(x, y: inteiro); 06) inicio 07) esse.x := x; 08) esse.y := y; 09) fim; 10) construtor constroi(x, y: inteiro); 11) inicio 12) setXY(x, y); 13) fim; 14)fim; 15) 16)var 17) ponto1: TPonto; 18) gi, gj: inteiro; 19)inicio 20) gi := 7; 21) gj := 7777; 22) criarobj(ponto1); 23) 24) ponto1.setXY(gi, gj); 25) 26) destruirobj(ponto1); 27)fim.
Quadro 52 - Exemplo de programa em FURBOL com chamada de método
A variável ponto1 é uma referência para o TAD TPonto . O método invocado chama-
se setXY . Nó código do método setXY pode-se observar o uso da palavra reservada esse
(Quadro 52, linhas 7 e 8), que foi usada neste caso para indicar ao compilador que os
símbolos x e y que estão sendo referenciados a esquerda das atribuições são os membros do
TAD e não os parâmetros x e y .
O código gerado para a chamada desse método é apresentado no Quadro 53.
Furbol assembly ponto1.setXY(gi, gj); 1) push gj
2) push gi 3) push ponto1 4) push bp 5) call TPonto@setXY
Quadro 53 - Código gerado para a chamada de um método
No Quadro 53 na linha 1 em assembly é empilhado o parâmetro gj . Na linha 2 o
parâmetro gi é empilhado. Na linha 3 é empilhado o conteúdo da variável do tipo objeto que
está sendo referenciada (ponto1 ). Na linha 4 é empilhado o registrador BP e finalmente o
método setXY é invocado.
Embora o método invocado chama-se setXY , em assembly o seu nome é formado pela
concatenação do nome do TAD, pelo símbolo @ e pelo seu nome em FURBOL. No exemplo
apresentado o nome em assembly do método é TPonto@setXY . Isso é necessário para evitar
conflitos no caso de outro TAD também possuir um método chamado setXY .
Um método também pode ser invocado por outro método do mesmo TAD. Isso pode
ser observado no exemplo do Quadro 52. Neste exemplo o método constroi invoca o
77
método setXY . Nestes casos basta o compilador propagar o parâmetro oculto esse para o
método chamado. O Quadro 54 apresenta o código completo gerado para o método constroi
do Quadro 52.
Furbol assembly construtor constroi(x, y: inteiro); inicio setXY(x, y); fim;
01) TPonto@constroi proc near 02) push bp 03) mov bp, sp 04) sub sp, 0 05) push [bp+10] 06) push [bp+8] 07) push word ptr [bp+6] 08) push bp 09) call TPonto@setXY 10) mov sp, bp 11) pop bp 12) ret 8 13) TPonto@constroi endp
Quadro 54 - Código para a chamada de um método por outro método
A linha 1 em assembly declara o inicio de um novo procedimento chamado de
TPonto@constroi . A linha 2 salva o conteúdo do registrador BP, pois o mesmo será alterado
em seguida. A linha 3 atribui ao registrador BP o endereço do topo da pilha (conteúdo do
registrador SP) que será usado durante o procedimento para endereçar valores na pilha. A
linha 4 reserva memória na pilha para as variáveis locais. A linha 5 empilha o parâmetro y . A
linha 6 empilha o parâmetro x . Na linha 7 o parâmetro oculto esse é propagado para o
método setXY. Essa linha enuncia que o conteúdo da palavra apontada pelo registrador BP
mais um deslocamento de 6 bytes deve ser empilhada, que no caso de um método é o
conteúdo do parâmetro oculto esse , conforme visto na seção 3.3.2. A linha 8 empilha o
conteúdo do registrador BP que é o elo de acesso dos procedimentos aninhados. Na linha 9 o
método setXY é invocado. A linha 10 restaura o conteúdo o conteúdo do registrador SP. A
linha 11 restaura o conteúdo do registrador BP empilhado na linha 2. A linha 12 enuncia o
retorno do procedimento indicando que o valor 8 (quantidade de bytes empilhado pelo
chamador do método constroi ) deve ser adicionado ao registrador SP. A linha 13 declara o
fim do procedimento.
3.3.4 Acesso aos atributos
Para acessar os atributos de um objeto adotou-se a seguinte estratégia. Primeiro é
atribuído ao registrador de segmento ES o conteúdo do parâmetro oculto esse , que no caso de
um programa escrito corretamente é o valor do segmento alocado para os atributos do objeto.
Em seguida o atributo é acessado usando a forma de endereçamento
78
ES:[deslocamento_do_atributo] , sendo deslocamento_do_atributo um número inteiro
calculado pelo compilador. Como o registrador de segmento de dados ES possui o valor do
segmento alocado previamente para o objeto, alterando-se o valor do deslocamento dentro do
segmento, pode-se acessar qualquer atributo do objeto.
Para calcular o deslocamento até um atributo o compilador consulta a tabela de
símbolos agregada ao TAD. O deslocamento de um determinado atributo é soma dos
tamanhos de todos os atributos que são definidos antes do atributo que está sendo calculado o
deslocamento.
O Quadro 55 apresenta a definição de um TAD que possui quatro atributos.
classe TRetangulo topo, esquerda, fundo, direita: inteiro; construtor constroi; inicio topo := 7; esquerda := 77; fundo := 777; direita := 7777; fim; fim;
Quadro 55 - Definição de um TAD com quatro atributos
No TAD definido no Quadro 55 os atributos topo , esquerda , fundo e direita
possuem respectivamente os deslocamentos 0, 2, 4 e 6.
No Quadro 56 é apresentado o código gerado para o método constroi do TAD
definido no Quadro 55.
Furbol assembly construtor constroi; inicio topo := 7; esquerda := 77; fundo := 777; direita := 7777; fim;
01) TRetangulo@constroi proc near 02) push bp 03) mov bp, sp 04) sub sp, 0 05) mov ax,7 06) mov es,word ptr [bp+6] 07) mov word ptr es:[0],ax 08) mov ax,77 09) mov es,word ptr [bp+6] 10) mov word ptr es:[2],ax 11) mov ax,777 12) mov es,word ptr [bp+6] 13) mov word ptr es:[4],ax 14) mov ax,7777 15) mov es,word ptr [bp+6] 16) mov word ptr es:[6],ax 17) mov sp, bp 18) pop bp 19) ret 4 20) TRetangulo@constroi endp
Quadro 56 - Código do método constroi definido no Quadro 55
Na linha 6 em assembly é atribuído ao registrador ES o conteúdo do parâmetro oculto
esse que é localizado na palavra apontada pelo registrador BP mais um deslocamento de 6
bytes, conforme visto na seção 3.3.2. Na linha 7 é atribuído para a palavra encontrada no
79
segmento ES com deslocamento 0 (deslocamento do atributo topo ) o conteúdo do registrador
AX. Desta forma o primeiro atributo do objeto recebe o conteúdo de AX. Código similar é
gerado nas linhas seguintes para acessar os outros atributos, alterando apenas o deslocamento
dos mesmos.
3.3.5 Palavra reservada nulo
Para o compilador a palavra reservada nulo é equivalente ao valor 0. O Quadro 57
apresenta o trecho de um programa que utiliza a palavra nulo .
01)programa teste; 02) 03)classe TPonto 04)(...) 05)fim; 06) 07)var 08) ponto: TPonto; 09) 10)inicio 11) (...) 12) 13) destruirobj(ponto); 14) ponto := nulo; 15) 16) (...) 17)fim.
Quadro 57 - Trecho de programa que utiliza a palavra reservada nulo
No programa apresentado a palavra reservada nulo é atribuída a variável global ponto
na linha 14 para indicar que a referência para o TAD não é mais válida, pois o objeto foi
excluído na linha 13. Quando a linha 14 for executada o conteúdo da variável ponto será 0.
No Quadro 58 é apresentado o código gerado pelo enunciado da linha 14 do Quadro
57.
Furbol assembly ponto := nulo; 1) mov ax,0
2) mov ponto,ax
Quadro 58 - Código gerado pelo enunciado da linha 14 do Quadro 57
Na linha 1 em assembly o valor 0 é atribuído ao registrador AX. Na linha 2 o conteúdo
do registrador AX é atribuído a variável global ponto .
3.3.6 Exclusão de objetos
A exclusão de um objeto (liberação da memória alocada para os seus atributos) é feita
80
de forma explicita através do comando destruirobj . O comando de exclusão espera como
primeiro parâmetro uma variável do tipo objeto e opcionalmente a chamada de um método
destrutor como segundo parâmetro.
No Quadro 59 é apresentado um programa que cria dois objetos do TAD TPonto e em
seguida exclui os dois objetos.
programa teste; classe TPonto x, y: inteiro; destrutor destroi; inicio imprime("Destruindo um ponto."); fim; fim; var ponto1, ponto2: TPonto; inicio criarobj(ponto1); criarobj(ponto2); destruirobj(ponto1); destruirobj(ponto2, destroi); fim.
Quadro 59 - Exemplo de programa que exclui dois objetos
Na primeira exclusão o destrutor do TAD não é invocado. Na segunda exclusão o
método destrutor destroi é invocado. No Quadro 60 é apresentado o código gerado para a
primeira exclusão.
Furbol assembly destruirobj(ponto1); 1) push ax
2) mov ax,ponto1 3) push ax 4) call destruirobj 5) pop ax
Quadro 60 - Exemplo de código gerado para exclusão de um objeto
Na linha 1 (Quadro 60) em assembly o conteúdo do registrador AX é salvo, pois o
mesmo será usado em seguida. Na linha 2 o conteúdo da variável global ponto1 é atribuído
ao registrador AX. A linha 3 empilha o conteúdo do registrador AX, que será o parâmetro do
procedimento destruirobj , que espera o valor do segmento do bloco previamente alocado
para o objeto que será liberado. A linha 4 invoca o procedimento destruirobj . Na linha 5 o
conteúdo do registrador AX é restaurado.
Quando a chamada de um método destrutor é enunciada, o compilador primeiro faz a
chamada do método e em seguida exclui o objeto. No Quadro 61 é apresentado o código
gerado para a segunda exclusão do programa do Quadro 59.
81
Furbol assembly destruirobj(ponto2, destroi); 1) push ponto2
2) push bp 3) call TPonto@destroi 4) push ax 5) mov ax,ponto2 6) push ax 7) call destruirobj 8) pop ax
Quadro 61 - Exemplo de código gerado para exclusão de um objeto com chamada de destrutor
Na linha 1 (Quadro 61) em assembly é empilhado o conteúdo da variável do tipo
objeto que está sendo referenciada (ponto2 , parâmetro oculto). Na linha 2 é empilhado o
conteúdo do registrador BP. Na linha 3 o método destrutor destroi é invocado. Na linha 4 o
conteúdo do registrador AX é salvo. Na linha 5 é atribuído ao registrador AX o conteúdo da
variável ponto2 . Na linha 6 o conteúdo do registrador AX é empilhado. A linha 7 invoca o
procedimento destruirobj . A linha 8 restaura o conteúdo do registrador AX.
3.3.6.1 Procedimento destruirobj
O procedimento destruirobj que é invocado pelo comando de mesmo nome é um
procedimento criado e que foi adicionado aos outros procedimentos do núcleo do FURBOL.
O procedimento recebe como parâmetro o valor do segmento do bloco previamente
alocado para o objeto e libera essa memória. Para liberar a memória é usada a função 49h
(Quadro 62) do MS-DOS.
Entradas: AH = 49h ES = segmento do bloco que será liberado Saída: CF = indica a ocorrência de erro (0 = ok) AX = código do erro, se houver Fonte: adaptado de Santos e Raymundi Júnior (1989, p. 245).
Quadro 62 - Uso da função 49h do MS-DOS
O Quadro 63 apresenta o trecho do procedimento no qual a memória é liberada. O
código completo da rotina pode ser encontrado no Apêndice A.
1) mov ah, 49h ;função do DOS 2) mov es, word ptr [bp + 4] ; es <- blocoMemoria 3) int 21h
Quadro 63 - Trecho do procedimento destruirobj
A linha 1 (Quadro 63) atribui ao registrador AH o código da função do MS-DOS que
será invocada. A linha 2 atribui ao registrador ES o valor do bloco de memória que será
liberado (parâmetro do procedimento). A linha 3 gera a interrupção que transfere a execução
para o MS-DOS.
82
3.4 ESPECIFICAÇÃO DO AMBIENTE
Nessa seção é apresenta a especificação do ambiente de programação FURBOL. Para
realizar a especificação do protótipo foi utilizada a Unified Modeling Language (UML),
através dos diagramas de casos de uso, classes e de seqüência.
3.4.1 Diagrama de casos de uso
Na Figura 4 é apresentado o diagrama de casos de uso do ambiente FURBOL.
Figura 4 - Diagrama de casos de uso
Os quatro casos de uso apresentados são:
a) Entrada do fonte : o usuário executa os comandos Abrir ou Novo da interface
do ambiente. Em seguida o sistema abre o arquivo escolhido ou limpa a tela de
edição do ambiente;
b) Salvar fonte : o usuário salva o fonte digitado ou modificado num arquivo .FUR;
c) Compilar : o usuário compila o fonte. No Quadro 64 este caso de uso é
apresentado com maiores detalhes;
d) Executar : o usuário executa o programa .COM gerado pelo ambiente.
83
COMPILAR
Descrição: usuário solicita que o programa seja compilado. Ator principal : usuário. Cenário principal: compilar a) através da interface o usuário escolhe a opção Compilar; b) o sistema compila o programa fonte; c) o sistema invoca o montador Turbo Assembler; d) o sistema invoca o ligador Turbo Link; e) o sistema exibe a mensagem “Programa compilado com sucesso”. Cenário de exceção: erro de compilação Caso o programa fonte apresente algum erro o sistema encerra a compilação e apresenta qual é o problema para o usuário. Cenário de exceção: erro na geração de código de máquina Caso ocorra algum erro durante a geração do código de máquina o sistema apresenta uma mensagem de erro para o usuário. Pré-condição: programa escrito em LP FURBOL. Pós-condição: programa em código de máquina criado.
Quadro 64 - Caso de uso compilar
3.4.2 Diagrama de classes
Na Figura 5 é apresentado o diagrama de classes do FURBOL. O diagrama
apresentado é simplificado (não apresenta os atributos e métodos das classes), pois o seu
objetivo é apresentar as associações entre as classes.
84
Figura 5 - Diagrama de classes do FURBOL
A função de cada classe do sistema é:
a) TFormPrincipal : representa a interface do sistema com o usuário;
b) TMontadorLigador : responsável por invocar o montador Turbo Assembler e o
ligador Turbo Link;
c) TMontadorLigadorSaidaFrm : responsável por apresentar a saída do montador e
do ligador para o usuário;
d) TAnalisadorLexico : implementa a parte de análise léxica do compilador.
Fornece o fluxo de tokens para o analisador sintático;
e) TAnalisadorSintatico : implementa um analisador sintático preditivo (seção
2.2.4.2) conforme a especificação da linguagem;
f) TGerenciadorSimbolos : representa a interface entre o analisador sintático e os
símbolos. A instalação, a busca, a atualização dos símbolos e o gerenciamento de
escopo são realizados através desta classe;
85
g) TListaTabelas : armazena as tabelas de símbolos usadas durante a tradução em
uma lista. No fim da compilação a memória alocada para todas as tabelas da lista é
liberada, garantindo desta forma que a memória alocada para os símbolos durante
a compilação também seja desalocada, evitando perda de memória;
h) TPilhaTabelas : armazena as tabelas de símbolos em uma pilha. As tabelas
encontradas mais no topo desta pilha possuem nível de alinhamento maior que as
encontradas no fundo da pilha;
i) TTabelaSimbolos : responsável pelos símbolos encontrados dentro de um escopo;
j) TSimbolo : representa um símbolo. Armazena informações sobre o símbolo tais
como: nome e tipo;
k) TDimensaoMatriz : classe auxiliar usada quando um símbolo é do tipo matriz.
3.4.3 Diagrama de seqüência para o caso de uso compilar
Na Figura 6 é apresentado o diagrama de seqüência para o caso de uso compilar.
86
Figura 6 - Diagrama de seqüência para o caso de uso compilar
O processo de compilação inicia com o usuário selecionando a opção Compilar na
interface do sistema. Em seguida o objeto FormPrincipal que é responsável pela interface do
sistema cria uma instância da classe TAnalisadorLexico (passando como parâmetro ao
construtor Create o programa fonte) e outra da classe TAnalisadorSintatico (passando a
instância do analisador léxico como parâmetro ao construtor Create ) que por sua vez cria
uma instância da classe TGerenciadorSimbolos .
Criadas as instâncias dos analisadores léxico e sintático o método Traduzir do
analisador sintático é invocado. Em seguida o analisador sintático entra em um laço até que o
fim do programa seja encontrado. É durante este laço que o programa em FURBOL é
traduzido. Enquanto o programa é traduzido o analisador sintático invoca repetidamente o
método ProximoToken do analisador léxico que se encarrega de fornecer o fluxo de tokens
para o analisador sintático. O analisador sintático também invoca métodos das classes
87
TGerenciadorSimbolos e TSimbolo conforme necessário.
Encerrada a tradução do programa fonte uma instância da classe TMontadorLigador é
criada. Em seguida os métodos Montar e Ligar são invocados, o que resulta na criação do
código de máquina executável em microprocessadores 8086/8088.
3.5 IMPLEMENTAÇÃO DO AMBIENTE
Nessa seção é apresentada a implementação do ambiente de programação FURBOL.
Para realizar a implementação do protótipo foi utilizado o ambiente de desenvolvimento
Borland Delphi 7.0.
O código do ambiente apresentado por Silva (2002) foi totalmente reaproveitado. A
interface do ambiente não foi alterada, bem como o código responsável pela mesma. O código
do compilador foi extendido para oferecer suporte a TADs definidos pelos usuários do
FURBOL, conforme a nova especificação da LP apresentada na seção 3.2.
3.5.1 Analisador léxico
O analisador léxico do FURBOL passou a reconhecer novas palavras reservadas. Para
classificar um token como palavra reservada o analisador léxico verifica se o token encontra-
se em um array constante de strings. A declaração deste array é apresentado no Quadro 65.
88
strLexReservadas : array [Low(TLexReservada)..High( TLexReservada)] of string = ( '', // prNenhum 'programa', // prPrograma 'inicio', // prInicio 'fim', // prFim 'var', // prVar 'inteiro', // prInteiro 'logico', // prLogico 'matriz', // prMatriz 'semaforo', // prSemaforo 'procedimento', // prProcedimento 'se', // prSe 'entao', // prEntao 'senao', // prSenao 'enquanto', // prEnquanto 'faca', // prFaca 'imprime', // prImprime 'leitura', // prLeitura 'verdadeiro', // prVerdadeiro 'falso', // prFalso 'nao', // prNao 'mod', // prMod 'div', // prDiv 'e', // prE 'ou', // prOu 'ref', // prRef 'inc', // prInc 'dec', // prDec 'nl', // prNl 'redimensiona', // prRedimensiona 'dimensoes', // prDimensoes, 'liminf', // prLiminf, 'limsup', // prLimsup, 'tarefa', // prTarefa 'morre', // prMorre 'espera', // prEspera 'repassa', // prRepassa 'criarsmf', // prCriarSmf 'excluirsmf', // prExcluirSmf 'p', // prP 'v', // prV 'criarobj', // prCriarObj 'destruirobj', // prDestruirObj 'classe', // prClasse 'construtor', // prConstrutor 'destrutor', // prDestrutor 'esse', // prEsse 'nulo'); // prNulo
Quadro 65 - Declaração das palavras reservadas do FURBOL
As palavras reservadas relacionadas com definição e uso de TADs são as sete últimas
palavras do array.
3.5.2 Analisador sintático preditivo
A classe que implementa o analisador sintático preditivo seguindo o algoritmo
apresentado na seção 2.2.4.2 é a classe TAnalisadorSintatico . Neste trabalho a classe foi
modificada para seguir a nova especificação da linguagem apresentada na seção 3.2. A classe
possui um método para cada não-terminal apresentado na especificação, além de outros
métodos auxiliares.
89
3.5.2.1 Exemplos da implementação de não-terminais da nova especificação da LP
Nessa seção são apresentados exemplos da implementação de três não-terminais da
nova especificação da LP, dando ênfase aos atributos herdados e sintetizados.
No Quadro 66 é apresentado a implementação do não-terminal TAD.
01)function TAnalisadorSintatico._TAD(var CodigoAsm : string): string; 02)var 03) TADName: string; 04) MetodosTADCodigo, 05) MetodosTADCodAsm: string; 06) Simb: TSimbolo; 07)begin 08) if FLexico.Token = tkIdentificador then begin 09) 10) if FSimbolos.SimboloRedeclarado(FLexico.Lexe ma) then 11) raise ESinErro.CreateFmt(strErrSinIdentDuplic ado, [FLexico.Linha, FLexico.Coluna, 12)FLexico.Lexema]); 13) 14) TADName := FLexico.Lexema; 15) 16) FLexico.ProximoToken; 17) 18) Simb := TSimbolo.Create(TADName, TipoSimbolo Classe); 19) FSimbolos.Instalar(Simb); 20) Simb.TabelaMembros := FSimbolos.AbrirEscopo( TADName); 21) 22) _AtributosTAD; 23) 24) MetodosTADCodigo := _MetodosTAD(Simb, Metodo sTADCodAsm); 25) 26) if (FLexico.Token = tkReservada) and (FLexic o.Reservada = prFim) then begin 27) FLexico.ProximoToken; 28) end 29) else 30) raise ESinErro.CreateFmt(strErrSinTokenEsp erado, [FLexico.Linha, FLexico.Coluna, 31)strLexReservadas[prFim]]); 32) 33) FSimbolos.FecharEscopo; 34) 35) Result := MetodosTADCodigo; 36) CodigoAsm := MetodosTADCodAsm; 37) 38) if (FLexico.Token = tkEspecial) and (FLexico .Lexema = ';') then begin 39) FLexico.ProximoToken; 40) end 41) else 42) raise ESinErro.CreateFmt(strErrSinTokenEsp erado, [FLexico.Linha, FLexico.Coluna, 43)';']); 44) end 45) else 46) raise ESinErro.CreateFmt(strErrSinIdentEsper ado, [FLexico.Linha, FLexico.Coluna]); 47)end;
Quadro 66 - Implementação do não-terminal TAD
O parâmetro CodigoAsm e o retorno da função são os atributos sintetizados do não-
terminal TAD e representam respectivamente o código assembly e intermediário gerados pela
definição do TAD.
As variáveis locais MetodosTADCodigo e MetodosTADCodAsm representam os atributos
sintetizados do não terminal MetodosTAD .
Nas linhas 8 a 12 é verificado se o token atual é um identificador e se o mesmo não foi
90
redeclarado dentro do espoco atual. Caso as condições não sejam satisfeitas um erro de
compilação é gerado. Detalhes sobre os erros de compilação são apresentados na seção
3.5.2.2.
A chamada do método ProximoToken da classe TAnalisadorLexico solicita ao
analisador léxico que o próximo token seja procurado. Esse token passará então a ser
referenciado como token atual.
Na linha 18 é criada uma instância da classe TSimbolo que manterá as informações do
TAD que está sendo definido. Na linha 19 o símbolo é instalado na tabela de símbolos do
escopo atual.
Na linha 20 é solicitado ao gerenciador de símbolos que um novo escopo seja aberto.
Esse novo escopo manterá os membros do TAD que está sendo definido. O método
AbrirEscopo do gerenciador de símbolos cria uma nova tabela de símbolos e empilha a nova
tabela na pilha de tabelas de símbolos. Esse escopo aberto passará então a ser referenciado
como escopo atual.
Na linha 22 é invocado o método que implementa o não-terminal AtributosTAD . Este
não-terminal não possui atributos herdados e sintetizados.
Na linha 24 o método que implementa o não-terminal MetodosTAD é invocado. É
passado como primeiro parâmetro a variável local que representa o atributo herdado do não-
terminal MetodosTAD (variável Simb ) e a variável que representa o atributo sintetizado do
mesmo (variável MetodosTADCodAsm) como segundo parâmetro. O outro atributo sintetizado
do não terminal MetodosTAD é o retorno do método invocado e é atribuído a variável local
MetodosTADCodigo que representa este atributo.
Na linha 33 é solicitado ao gerenciador de símbolos que o escopo aberto na linha 20
seja encerrado. O método FecharEscopo do gerenciador de símbolos desempilha uma tabela
de símbolos da pilha de tabelas.
Nas linhas 35 e 36 os atributos sintetizados do não-terminal TAD são produzidos.
No Quadro 67 é apresentado a implementação do não-terminal MetodoTAD.
91
01)function TAnalisadorSintatico._MetodoTAD(TAD: TS imbolo; TipoMetodo: TTipoMetodo; 02) var Cod igoAsm: string): string; 03)var 04) MetodoName, MetodoNameAsm: string; 05) Simb, ParamOculto: TSimbolo; 06) TipoParamOculto: TTipoSimbolo; 07) Proximo, CCompostoCodigo, CCompostoCodAsm: str ing; 08)begin 09) if FLexico.Token = tkIdentificador then begin 10) if FSimbolos.SimboloRedeclarado(FLexico.Lexe ma) then 11) raise ESinErro.CreateFmt(strErrSinIdentDupli cado,[FLexico.Linha,FLexico.Coluna, 12)FLexico.Lexema]); 13) 14) MetodoName := FLexico.Lexema; 15) MetodoNameAsm := TAD.Nome + '@' + FLexico.Le xema; 16) FLexico.ProximoToken; 17) 18) Simb := TSimbolo.Create(MetodoName, TipoSimb oloMetodo); 19) Simb.NomeMetodoAsm := MetodoNameAsm; 20) FSimbolos.Instalar(Simb); 21) Simb.TabelaAgregada := FSimbolos.AbrirEscopo (MetodoName); 22) Simb.TipoMetodo := TipoMetodo; 23) 24) TipoParamOculto.Tipo := tsObjeto; 25) TipoParamOculto.TAD := TAD; 26) ParamOculto := TSimbolo.Create(strLexReserva das[prEsse], TipoParamOculto); 27) FSimbolos.Instalar(ParamOculto); 28) ParamOculto.TipoVariavel := tvValor; 29) 30) _ParamFormais; 31) 32) if (FLexico.Token = tkEspecial) and (FLexico .Lexema = ';') then begin 33) FLexico.ProximoToken; 34) 35) _EstruturaDados(tvLocal); 36) 37) CCompostoCodigo := _CComposto(Proximo, CCo mpostoCodAsm); 38) 39) Result := CRLF + 40) MontaLinha(MetodoNameAsm, 'proc' , 'near', true) + 41) CCompostoCodigo + 42) MontaLinha('', 'ret', '', true) + 43) MontaLinha(MetodoNameAsm, 'endp' , '', true); 44) 45) CodigoAsm := CRLF + 46) MontaLinha(MetodoNameAsm, 'proc', 'near' , true) + 47) MontaLinha('', 'push', 'bp', true) + 48) MontaLinha('', 'mov', 'bp, sp', true) + 49) MontaLinha('', 'sub', 'sp, ' + 50)IntToStr(FSimbolos.EscopoAtual.LarguraVariaveis) , true) + 51) CCompostoCodAsm + 52) MontaLinha('', 'mov', 'sp, bp', true) + 53) MontaLinha('', 'pop', 'bp', true) + 54) MontaLinha('', 'ret', IntToStr(FSimbolos .EscopoAtual.LarguraParametros + 2), 55)true) + 56) MontaLinha(MetodoNameAsm, 'endp', '', tr ue); 57) 58) FSimbolos.FecharEscopo; 59) 60) if (FLexico.Token = tkEspecial) and (FLexi co.Lexema = ';') then begin 61) FLexico.ProximoToken; 62) end 63) else 64) raise 65) ESinErro.CreateFmt(strErrSinTokenEsperado, [FLexico.Linha,FLexico.Coluna,';']); 66) end 67) else 68) raise 69)ESinErro.CreateFmt(strErrSinTokenEsperado,[FLexi co.Linha,FLexico.Coluna,';']); 70) end 71) else 72) raise ESinErro.CreateFmt(strErrSinIdentEsper ado, [FLexico.Linha, FLexico.Coluna]); 73)end;
Quadro 67 - Implementação do não-terminal MetodoTAD
Os parâmetros do método TAD e TipoMetodo representam os parâmetros herdados do
92
não-terminal MetodoTAD (linha 1). O primeiro é uma referência para o TAD ao qual o método
que está sendo definido pertence. O segundo é o tipo do método o qual pode ser construtor,
procedimento (atualizador ou consultor) ou destrutor. O parâmetro CodigoAsm e o retorno da
função são os atributos sintetizados do não-terminal MetodoTAD e representam o código
assembly e intermediário gerados respectivamente.
As variáveis locais Proximo , CCompostoCodigo e CCompostoCodAsm representam os
atributos sintetizados do não terminal CComposto .
Na linha 15 é estabelecido o nome em assembly do método do TAD que está sendo
definido.
Na linha 18 é criada uma instância da classe TSimbolo que vai manter as informações
do método que está sendo definido. Na linha 20 o símbolo é instalado na tabela de símbolos
do escopo atual.
Na linha 21 é solicitado ao gerenciador de símbolos que um novo escopo seja aberto.
Este novo escopo vai manter os parâmetros e variáveis locais do método que está sendo
definido.
Nas linhas 24 a 28 o parâmetro oculto esse é criado e instalado no escopo aberto na
linha 21. O parâmetro oculto é instalado como primeiro parâmetro do método do TAD. O seu
tipo é objeto e aponta para o TAD ao qual o método que está sendo definido pertence.
Na linha 30 é invocado o método que implementa o não-terminal ParamFormais . Na
linha 35 o método que implementa o não-terminal EstruturaDados . Para este método é
passado um parâmetro indicando que as variáveis que estão sendo declaradas são variáveis
locais.
Na linha 37 o método que implementa o não-terminal CComposto é invocado. É
passado para o método as variáveis locais que representam os atributos sintetizados do não-
terminal CComposto .
Nas linhas 39 a 43 é montado o código intermediário do método do TAD que está
sendo definido. Nas linhas 45 a 56 é montado o código assembly do método.
Na linha 58 é solicitado ao gerenciador de símbolos que o escopo aberto na linha 21
seja encerrado.
No Quadro 68 é apresentado a implementação do não-terminal Esse .
93
01)function TAnalisadorSintatico._Esse: TSimbolo; 02)begin 03) if (FLexico.Token = tkEspecial) and (FLexico.L exema = '.') then begin 04) FLexico.ProximoToken; 05) 06) if (FLexico.Token = tkIdentificador) then be gin 07) 08) // busca um nivel acima (atributo ou metod o) 09) Result := FSimbolos.ProcurarSimboloNivel(F Lexico.Lexema, 10)FSimbolos.EscopoAtual.Nivel-1); 11) if not Assigned(Result) then 12) raise ESinErro.CreateFmt(strErrSinIdentN aoDeclarado, [FLexico.Linha, 13)FLexico.Coluna, FLexico.Lexema]); 14) 15) FLexico.ProximoToken; 16) 17) end 18) else 19) raise ESinErro.CreateFmt(strErrSinIdentEsp erado, [FLexico.Linha, 20)FLexico.Coluna]); 21) end 22) else begin 23) Result := FSimbolos.ProcurarSimbolo(strLexRe servadas[prEsse]); 24) if not Assigned(Result) then 25) raise ESinErro.CreateFmt(strErrSinIdentNao Declarado, [FLexico.Linha, 26)FLexico.Coluna, FLexico.Lexema]); 27) end; 28)end;
Quadro 68 - Implementação do não-terminal Esse
O não-terminal Esse possui um atributo sintetizado que é o retorno do método.
Como o não-terminal Esse possui duas produções, o analisador sintático deve decidir
qual delas usar baseado no token atual. Esta tarefa é realizada na linha 3. Caso a condição
apresentada nessa linha seja satisfeita, a primeira produção é usada. Caso contrário a segunda.
Nas linhas 9 e 10 o método ProcurarSimboloNivel é invocado. Este método procura
um símbolo exclusivamente no nível passado como segundo parâmetro. Neste caso é
especificado que o nível deve ser um a menos que o nível atual. Como a palavra reservada
esse só pode ser usada dentro de um método, esta chamada ao método
ProcurarSimboloNivel garante que o símbolo será procurado exclusivamente no nível onde
os membros do TAD estão localizados.
Na linha 23 é solicitado ao gerenciador de símbolos que o símbolo com nome esse
seja localizado, ou seja, o parâmetro oculto esse é procurado.
No Quadro 69 é apresentado o código do método ProcurarSimboloNivel do
gerenciador de símbolos.
1)function TGerenciadorSimbolos.ProcurarSimboloNive l(const Nome: String; Nivel: Integer): 2)TSimbolo; 3)begin 4) Result := nil; 5) if (Nivel >= 0) and (Nivel < FEscopoAtivo.Quant idade) then 6) begin 7) Result := FEscopoAtivo.Tabelas[Nivel].Procura rSimbolo(Nome); 8) end; 9)end;
Quadro 69 - Código do método ProcurarSimboloNivel do gerenciador de símbolos
A procura é realizada exclusivamente na tabela encontrada no nível da pilha de tabelas
94
(atributo FEscopoAtivo da classe) que foi especificado como parâmetro (parâmetro Nível -
Quadro 69, linha 1). Caso o nível especificado seja inválido, a linha 7 não é executada e nil é
retornado.
Na linha 7 é invocado o método ProcurarSimbolo da classe TTabelaSimbolos . Este
método verifica se existe um símbolo na tabela com o nome igual ao passado como
parâmetro.
3.5.2.2 Erros de compilação
O compilador do FURBOL não implementa recuperação de erros. Quando um erro de
compilação é encontrado o compilador lança uma exceção encerrando o processo de tradução.
A exceção é capturada pela classe TFormPrincipal que se encarrega de reportar o erro para
o usuário. O Quadro 70 apresenta um trecho do código do compilador que lança uma exceção.
1)if (FLexico.Token = tkEspecial) and (FLexico.Lexe ma = ';') then begin 2) FLexico.ProximoToken; 3)end 4)else 5)raise 6) ESinErro.CreateFmt(strErrSinTokenEsperado,[FLex ico.Linha,FLexico.Coluna,';']);
Quadro 70 - Exemplo de exceção gerada pelo compilador
No código apresentado é verificado na linha 1 se o token atual é o token especial ‘;’ .
Caso a condição não seja satisfeita o código das linhas 5 e 6 é executado, o que lança uma
exceção encerrando a compilação.
3.5.2.3 Acesso aos atributos do TAD
O método do compilador que gera código que atribui ao registrador de segmento ES o
conteúdo do parâmetro oculto esse é apresentado no Quadro 71.
function TAnalisadorSintatico.GerarAcessoAtributos: string; begin { carrregar o registrador ES com o primeiro paramet ro do método (parametro oculto que possui o valor do segmento on de estão os atributos do TAD) } Result := MontaLinha('', 'mov', 'es,word ptr [bp+' + IntToStr(FSimbolos.EspacoRes ervadoParametros) + ']', true); end;
Quadro 71 – Método que gera código que atribui ao registrador de segmento ES o conteúdo do parâmetro esse
Como o valor do atributo EspacoReservadoParametros não se altera durante toda a
compilação (é sempre 6), o código gerado para atribuir ao registrador de segmento ES o
95
conteúdo do parâmetro esse é sempre o mesmo, conforme o que foi apresentado na seção
3.3.2.
O método que gera código para acessar os atributos do TAD usando o registrador ES
como registrador de segmento é apresentado no Quadro 72.
01)function TSimbolo.ObtemCodigoAssemblyAtributo(Re gDeslocInt: String): string; 02)begin 03) Result := ''; 04) 05) case Tipo.Tipo of 06) tsLogico, tsSemaforo: Result := Result + 'by te ptr '; 07) else Result := Result + 'word ptr '; 08) end; 09) 10) Result := Result + 'es:['; 11) 12) if (RegDeslocInt <> '') then begin 13) Result := Result + RegDeslocInt + '+'; 14) end; 15) 16) Result := Result + IntToStr(Deslocamento) + '] '; 17) 18)end;
Quadro 72 - Método que gera código assembly para acessar os atributos de um TAD
Nas linhas 5 a 8 (Quadro 72) é decidido se será gerado código para endereçar um ou
dois bytes, conforme o tipo do atributo. Na linha 16 é gerado o código para acessar o atributo.
Para isso é calculado o deslocamento do atributo dentro do TAD (seção 3.3.4) acessando a
property Deslocamento do símbolo. O parâmetro RegDeslocInt possui um deslocamento
adicional que quando não for vazio deve ser adicionado ao deslocamento do atributo. Esse
deslocamento adicional é usado quando o atributo é um array.
Os métodos apresentados no Quadro 71 e no Quadro 72 são usados em conjunto pelo
compilador. Primeiro é invocado o método do Quadro 71 para atribuir ao registrador ES o
conteúdo do parâmetro esse e em seguida o método do Quadro 72 para gerar código para
acessar o atributo. No Quadro 73 é apresentado um trecho de código do compilador que
invoca os métodos apresentados no Quadro 71 e no Quadro 72.
if (Simb.TipoVariavel = tvAtributo) then begin Prelocal := GerarAcessoAtributos; Local := Simb.ObtemCodigoAssemblyAtributo(''); End
Quadro 73 – Trecho de código do compilador que invoca os métodos apresentados no Quadro 71 e no Quadro 72
3.6 OPERACIONALIDADE DO AMBIENTE
O ambiente de programação FURBOL permite que o usuário abra, salve ou crie um
96
novo arquivo fonte. Com o fonte criado o usuário pode compilar o programa, executá-lo,
visualizar o código intermediário e assembly gerado. A Figura 7 apresenta como acessar as
principais funcionalidades do FURBOL.
Figura 7 - Acesso as principais funcionalidades do FURBOL
As operações Novo, Abrir e Salvar também podem ser acessadas através do menu
Arquivo do ambiente. As operações Compilar e Executar também são encontradas no menu
Projeto .
Selecionando a segunda aba do editor, o usuário pode visualizar o código intermediário
gerado pelo processo de tradução. A Figura 8 apresenta o ambiente exibindo o código
intermediário gerado pelo compilador.
97
Figura 8 - Ambiente exibindo o código intermediário gerado pelo compilador
Através da terceira aba do editor é exibido o código de montagem gerado ao usuário. A
Figura 9 apresenta o ambiente exibindo o código de montagem gerado durante a compilação.
98
Figura 9 - Ambiente exibindo o código de montagem gerado pelo compilador
Outra funcionalidade disponibilizada pelo ambiente é visualizar qual foi a saída
apresentada pelo montador e pelo ligador (Figura 10). O usuário pode executar essa operação
através do menu Projeto , opção Saída montador/ligador .
99
Figura 10 - Ambiente exibindo a saída do montador e do ligador
Algumas das funcionalidades do FURBOL também podem ser acessadas através de
atalhos no teclado. O Quadro 74 apresenta estas operações e seus respectivos atalhos.
Operação Atalho Novo arquivo Ctrl + N Abrir arquivo Ctrl + A Salvar arquivo Ctrl + S Compilar Ctrl + F9 Executar F9 Saída montador/ligador F5
Quadro 74 - Atalhos no teclado do ambiente
3.7 RESULTADOS E DISCUSSÃO
Desde o inicio da implementação percebeu-se que os testes seriam dificultados sem um
comando de saída que imprimisse inteiros. O comando de saída do FURBOL apenas imprimia
cadeias de caracteres. Com o objetivo de facilitar os testes e de ampliar a LP FURBOL foi
implementado um procedimento que imprime inteiros (Apêndice B) e que foi adicionado aos
outros procedimentos do núcleo do FURBOL.
Durante a implementação e dos testes percebeu-se problemas com a geração de código
de algumas construções já existentes no FURBOL e que acabaram não sendo corrigidas, os
quais são:
a) os parâmetros por referência só funcionam corretamente com variáveis globais. A
100
saída do programa do Quadro 75, por exemplo, é indefinida, enquanto o correto
seria imprimir o valor 7777;
programa bug_param_referencia; procedimento proc1(ref i: inteiro); inicio i := 7777; fim; procedimento proc2; var j: inteiro; inicio proc1(j); imprime(j); (* resultado indefinido *) fim; inicio proc2; fim.
Quadro 75 – Exemplo de problema com parâmetros por referência
b) expressões que possuem parênteses apresentam em alguns casos um resultado
incorreto. O programa do Quadro 76, por exemplo, apresenta como saída o valor 4,
enquanto o correto seria 5.
programa bug_expressao; var i: inteiro; inicio i := (1 + 1) + 1 + (1 + 1); imprime(i); (* imprime 4 *) fim.
Quadro 76 - Exemplo de uma expressão que apresenta um resultado incorreto
Outro problema encontrado foi que o compilador não verificava durante a chamada de
um método se a quantidade de parâmetros reais era o mesmo que a quantidade de parâmetros
formais. Uma consistência foi adicionada e este problema foi corrigido.
A implementação existente de arrays não foi completamente adaptada a definição e
uso de TADs, deixando este trabalho com limitações, as quais são:
a) não é permitido declarar arrays dinâmicos como atributos dos TADs (Quadro 77,
linha 13);
b) é possível declarar um array de objetos, mas não é possível usar os elementos do
array individualmente nos comandos criarobj , destruirobj e na chamada de
métodos (Quadro 77, linhas 19, 20 e 21).
101
01)programa limitacoes_arrays; 02) 03)classe TPonto 04) x, y: inteiro; 05) procedimento setXY(x, y: inteiro); 06) inicio 07) esse.x := x; 08) esse.y := y; 09) fim; 10)fim; 11) 12)classe TPilha 13) fPilha: matriz; (* não compila *) 14)fim; 15) 16)var 17) gPontos: matriz[1..120]: TPonto; 18)inicio 19) criarobj(gPontos[1]); (* não compila *) 20) gPontos[1].setXY(7, 120); (* não compila *) 21) destruirobj(gPontos[1]); (* não compila *) 22)fim.
Quadro 77 - Limitações do trabalho com relação aos arrays
Outra limitação do trabalho é que um símbolo não pode ser usado antes que o mesmo
seja definido. Por exemplo, um método não pode invocar outro método do mesmo TAD antes
que este método seja definido (Quadro 78, linha 15). Outro exemplo similar é que um TAD
não pode possuir como atributo um objeto de um TAD ainda não definido (Quadro 78, linha
5).
01)programa limitacao_simbolo_nao_definido; 02) 03)classe TRetangulo 04) topo_esquerda, 05) fundo_direita: TPonto; (* não compila – Tponto ainda não foi definido *) 06) 07) (...) 08)fim; 09) 10)classe TPonto 11) x, y: inteiro; 12) 13) construtor constroi(x, y: inteiro); 14) inicio 15) setXY(x, y); (* não compila – setXY ainda não foi definido *) 16) fim; 17) 18) procedimento setXY(x, y: inteiro); 19) inicio 20) esse.x := x; 21) esse.y := y; 22) fim; 24)fim; 23) 24)inicio 25) (...) 26)fim.
Quadro 78 - Limitações do trabalho com relação ao uso de símbolos ainda não definidos
Para validar a implementação, primeiro foram realizados testes nos procedimentos
adicionados ao núcleo (imprime , criarobj e destruirobj ). Quando os testes dos
procedimentos do núcleo foram concluídos partiu-se para a implementação do compilador.
Conforme os novos não-terminais ou as mudanças nas ações semânticas da
especificação da linguagem foram implementados, pequenos trechos de programas em
102
FURBOL foram escritos e o código gerado analisado e validado. Quando tornou-se possível
definir TADs completos e código para usá-los, foram escritos pequenos programas que
testavam esses TADs. Novos programas foram escritos conforme a extensão do FURBOL era
implementada. Sempre que uma mudança significativa era implementada ou um erro
encontrado e corrigido, todos os programas de testes eram novamente verificados.
103
4 CONCLUSÕES
O objetivo deste trabalho foi alcançado. A LP foi estendida e tornou-se possível a
criação de TADs pelos usuários do FURBOL. Esse pode definir os quatro tipos de operações
de um TAD (construtoras, consultoras, atualizadoras e destrutoras), declarar referências para
os TADs em outras unidades do programa e criar instâncias destes TADs.
Contudo, o trabalho apresenta algumas limitações, as quais são:
a) não é permitido declarar arrays dinâmicos como atributos dos TADs;
b) não é possível usar os elementos de um array de objetos individualmente nos
comandos criarobj , destruirobj e na chamada de métodos;
c) um símbolo não pode ser utilizado antes de ser definido.
As ferramentas utilizadas (Enterprise Architect para a especificação do ambiente e
Borland Delphi 7.0 para a implementação) mostraram-se adequadas.
Este trabalho é relevante para o curso de Ciência da Computação da FURB, pois
apresenta a extensão de um trabalho de pesquisa que vem sendo desenvolvida por vários
acadêmicos do curso e aprofunda vários temas abordados durante o curso de computação.
4.1 EXTENSÕES
Como possíveis extensões para o trabalho sugere-se:
a) ampliar a LP para oferecer suporte a orientação a objetos;
b) ampliar a LP para oferecer suporte a tratamento de exceções;
c) atualmente o compilador encerra a tradução quando um erro é encontrado (seção
3.5.2.2). Sugere-se implementar a recuperação de erros para que a compilação
continue até o final do arquivo fonte mesmo quando erros forem detectados;
d) criar uma biblioteca com funções de entrada e saída para vários dispositivos;
e) otimizar o código gerado;
f) ampliar a LP para permitir a definição de funções;
g) gerar código para outras plataformas (microcontroladores, ARM, entre outros).
104
REFERÊNCIAS BIBLIOGRÁFICAS
ADRIANO, Anderson. Implementação de mapeamento finito (array’s) no ambiente FURBOL . 2001. 94 f. Trabalho de Conclusão de Curso (Bacharelado em Ciências da Computação) - Centro de Ciências Exatas e Naturais, Universidade Regional de Blumenau, Blumenau.
AHO, Alfred V.; SETHI, Ravi; ULMAN, Jeffrey D. Compiladores: princípios, técnicas e ferramentas. Tradução Daniel de Ariosto Pinto. Rio de Janeiro: Livros Técnicos e Científicos, 1995.
ANDRÉ, Geovânio B. Protótipo do gerador executável a partir do ambiente FURBOL. 2000. 65 f. Trabalho de Conclusão de Curso (Bacharelado em Ciências da Computação) - Centro de Ciências Exatas e Naturais, Universidade Regional de Blumenau, Blumenau.
BRUXEL, Jorge L. Definição de um interpretador para a linguagem Portugol, utilizando gramática de atributos. 1996. 67 f. Trabalho de Conclusão de Curso (Bacharelado em Ciências da Computação) - Centro de Ciências Exatas e Naturais, Universidade Regional de Blumenau, Blumenau.
LEYENDECKER, Gustavo Z. Especificação e compilação de uma linguagem de programação orientada a objetos para a plataforma Microsoft .NET. 2005. 88 f. Trabalho de Conclusão de Curso (Bacharelado em Ciências da Computação) - Centro de Ciências Exatas e Naturais, Universidade Regional de Blumenau, Blumenau.
LOUDEN, Kenneth C. Compiladores: princípios e práticas. Tradução Flávio Soares Corrêa da Silva. São Paulo: Thomson Pioneira, 2004.
NORTON, Peter; WILTON, Richard. Novo guia Peter Norton para programadores do IBM PC & OS/2. Tradução Daniel Vieira. Rio de Janeiro: Campus, 1991.
PRICE, Ana M. A.; TOSCANI, Simão S. Implementação de linguagens de programação: compiladores. 2. ed. Porto Alegre: Sagra Luzzatto, 2001.
RADLOFF, Marcelo. Protótipo de um ambiente para programação em uma linguagem bloco estruturada com vocabulário na língua portuguesa. 1997. 73 f. Trabalho de Conclusão de Curso (Bacharelado em Ciências da Computação) - Centro de Ciências Exatas e Naturais, Universidade Regional de Blumenau, Blumenau.
SANTOS, Jeremias P. dos; RAYMUNDI JÚNIOR, Edison. Programando em assembler: 8086/8088. São Paulo: McGraw-Hill, 1989.
105
SCHMITZ, Héldio. Implementação de produto cartesiano e métodos de passagem de parâmetros no ambiente FURBOL. 1999. 86 f. Trabalho de Conclusão de Curso (Bacharelado em Ciências da Computação) - Centro de Ciências Exatas e Naturais, Universidade Regional de Blumenau, Blumenau.
SEBESTA, Robert W. Conceitos de linguagens de programação. Tradução José Carlos Barbosa dos Santos. Porto Alegre: Bookman, 2000.
SILVA, José R. V. da; SILVA, Joilson M. da. Desenvolvimento de um ambiente de programação para a linguagem Portugol. Dynamis, Blumenau, v. 1, n. 5, p. 99-114, dez. 1993.
SILVA, Paulo H. Implementação de unidades para processos concorrentes no ambiente FURBOL. 2002. 154 f. Trabalho de Conclusão de Curso (Bacharelado em Ciências da Computação) - Centro de Ciências Exatas e Naturais, Universidade Regional de Blumenau, Blumenau.
VAREJÃO, F. Linguagens de programação: Java, C, C++ e outras: conceitos e técnicas. Rio de Janeiro: Campus, 2004.
TOMAZELLI, Giancarlo. Implementação de um compilador para uma linguagem de programação com geração de código Microsoft .NET Intermediate Language. 2004. 83 f. Trabalho de Conclusão de Curso (Bacharelado em Ciências da Computação) - Centro de Ciências Exatas e Naturais, Universidade Regional de Blumenau, Blumenau.
VARGAS, Douglas N. Editor dirigido por sintaxe . Relatório de pesquisa n. 240 arquivado na Pró-Reitoria de Pesquisa da Universidade Regional de Blumenau, Blumenau, set. 1992.
______. Definição e implementação no ambiente windows de uma ferramenta para o auxílio no desenvolvimento de programas. 1993. 114 f. Trabalho de Conclusão de Curso (Bacharelado em Ciências da Computação) - Centro de Ciências Exatas e Naturais, Universidade Regional de Blumenau.
106
APÊNDICE A – Procedimentos do núcleo para criação e exclusão de objetos
No Quadro 79 é apresentado o código completo do procedimento do núcleo criarobj .
;-------------------------------------------------- --------- ; NÚCLEO: Alocar memória para o objeto - FURBOL: criarobj(tamanhoObj: inteiro);
Retorna no AX o valor do segmento do bloco alocado para o objeto ;-------------------------------------------------- --------- criarobj proc near push bp mov bp, sp ;salva registradores push bx push cx push dx push si push di ;calcula quantos parágrafos são necessários para o objeto mov ax, word ptr[bp+4] ; ax <- tamanhoObj ;divide ax por 16 mov bx, 16 cwd div bx ;ax possui a quantidade de parágrafos ;se tem resto soma mais um parágrafo cmp dx, 0 jz criarobj_label1 add ax, 1 criarobj_label1: ;aloca pelo menos um parágrafo cmp ax, 0 jnz criarobj_label2 add ax, 1 criarobj_label2: ;aloca a memória mov bx, ax ; bx <- quantidade de parágrafos mov ah, 48h ;função do DOS int 21h jc criarobj_erro jmp criarobj_fim ; ax contém o valor do segmento do bloco alocado (retorno da função)
criarobj_erro: mov ah, 9 lea dx, erro@criar@objeto int 21h ; emite a mensagem de erro mov ax, 0 criarobj_fim: ;recupera registradores pop di pop si pop dx pop cx pop bx mov sp, bp pop bp ret 2 criarobj endp
Quadro 79 - Procedimento do núcleo criarobj
107
No Quadro 80 é apresentado o código completo da rotina do núcleo destruirobj .
;-------------------------------------------------- --------- ; NÚCLEO: Libera a memória alocada para o objeto - FURBOL: destruirobj(blocoMemoria: inteiro); ;-------------------------------------------------- --------- destruirobj proc near push bp mov bp, sp ;salva registradores push ax push bx push cx push dx push si push di push es mov ah, 49h ;função do DOS mov es, word ptr [bp + 4] ; es <- blocoMemoria int 21h jc destruirobj_erro jmp destruirobj_fim destruirobj_erro: mov ah, 9 lea dx, erro@destruir@objeto int 21h destruirobj_fim: ;recupera registradores pop es pop di pop si pop dx pop cx pop bx pop ax mov sp, bp pop bp ret 2 destruirobj endp
Quadro 80 - Procedimento do núcleo destruirobj
108
APÊNDICE B – Procedimento do núcleo para imprimir inteiros
No Quadro 81 é apresentado o código do procedimento usado para imprimir inteiros.
;-------------------------------------------------- --------- ; NÚCLEO: Imprimir inteiro - FURBOL: imprime(i: int eiro); ;-------------------------------------------------- --------- tam_buffer_imprime equ 12 imprime proc near push bp mov bp, sp ;variaveis locais sub sp, tam_buffer_imprime ;buffer para guardar os caracteres ;salva registradores push ax push bx push cx push dx push si push di mov ax, word ptr[bp+4] ; ax <- i mov si, -1 ;indexador do buffer ;calcula o abs de ax cwd xor ax, dx sub ax, dx loop_divide: ;divide ax por 10 mov bx, 10 cwd div bx add dl, 48 ;transforma o resto da divisão em u m caractere ascii add si, 1 mov byte ptr[bp-tam_buffer_imprime+si], dl ;guar da o caractere no buffer de caracteres ;testa se a divisão acabou cmp ax, 0 jne loop_divide ;se o parâmetro i for negativo imprime o caracte r '-' cmp word ptr[bp+4], 0 jnl loop_imprime mov ah, 2 mov dl, 45 int 21h ;imprime os caracteres do buffer loop_imprime: ;imprime caracter na tela mov dl, byte ptr[bp-tam_buffer_imprime+si] mov ah, 2 int 21h ;testa se os caracteres acabaram sub si, 1 cmp si, 0 jnl loop_imprime ;imprime quebra de linha mov ah, 2 mov dl, 13 int 21h mov dl, 10 int 21h ;recupera registradores pop di pop si pop dx pop cx pop bx pop ax mov sp, bp pop bp ret 2 imprime endp
Quadro 81 - Procedimento do núcleo imprime