IMPLEMENTAÇÃO DE UM SISTEMA DE
PROGRAMAÇÃO EM LÓGICA
CONTEXTUAL
Artur Miguel Dias
UNL-DI 26/90
Setembro de 1990
Esta tese foi submetida ao Departamento de Engenharia Electrotécnica e de
Computadores do INSTITUTO SUPERIOR TÉCNICO para satisfação parcial dos
requisitos
do
programa
de
mestrado
em
Engenharia
Computadores.
Departamento de Informática
Universidade Nova de Lisboa
2825 Monte da Caparica
Portugal
Electrotécnica
e
de
IMPLEMENTAÇÃO DE UM SISTEMA DE
PROGRAMAÇÃO EM LÓGICA
CONTEXTUAL
Artur Miguel de Andrade Vieira Dias
licenciado em Engenharia Informática
pela
FACULDADE DE CIÊNCIAS E TECNOLOGIA
UNIVERSIDADE NOVA DE LISBOA
Tese submetida para satisfação parcial dos requisitos do
programa de mestrado
em
Engenharia Electrotécnica e de Computadores
(Computadores)
INSTITUTO SUPERIOR TÉCNICO
Departamento de Engenharia Electrotécnica e de Computadores
LISBOA
Setembro de 1990
Tese realizada sob a supervisão de
Professor Doutor Luís Fernando Lopes Monteiro
Professor Associado do Departamento de Informática
FACULDADE DE CIÊNCIAS E TECNOLOGIA
UNIVERSIDADE NOVA DE LISBOA
e de
Professor Doutor Pedro Manuel Barbosa Veiga
Professor Associado do Departamento Engenharia Electrotécnica e de Computadores
INSTITUTO SUPERIOR TÉCNICO
UNIVERSIDADE TÉCNICA DE LISBOA
2
Resumo
Programação em lógica contextual, ou CxLP, é um novo esquema de programação
que enriquece o paradigma da programação em lógica com mecanismos de
estruturação dos programas em units e de controlo do processo de derivação com base
no agrupamento de units em entidades dinâmicas chamadas contextos. Neste
trabalho descrevemos a implementação de um sistema de programação em lógica
contextual, compatível com o Prolog, portável, e muito eficiente e flexível. No seu
coração encontra-se uma máquina abstracta altamente optimizada, chamada CxM,
desenvolvida a partir da WAM de D.Warren, cuja velocidade atinge os 60 Klips numa
VAXstation 3540 com o programa de teste naïve-reverse. Algum conforto
e
flexibilidade de utilização foram conseguidos como resultado do desenvolvimento de
um compilador incremental rápido e da utilização de mecanismos de ligação
incrementais e reversíveis. Do ponto de vista funcional, o sistema tem todas as
propriedades de um interpretador.
Palavras chave:
Programação em lógica, programação em lógica contextual,
compilação incremental, ligação, implementação, contexto.
Abstract
Contextual logic programming (CxLP), is a new programming scheme that extends
the logic programming paradigm with mechanisms to structure programs into units
and to control the derivation process by grouping units in dynamic entities named
contexts.
In
this
work
a
Prolog
compatible,
portable,
fast
and
flexible
implementation of a contextual logic programming system is described. In its heart
there is an highly optimized abstract machine, named CxM, developed as an
extension of D. Warren’s WAM, that attains 60 Klips on a VAXstation 3540 using the
standard naïve-reverse benchmark. Some convenience and flexibility was achieved
as the result of the development of a fast incremental compiler and the use of
incremental and reversible binding mechanisms. Functionally, our system enjoys all
the properties of an interactive interpreter .
Keywords:
Logic programming, contextual logic programming, incremental
compilation, binding, implementation, context.
3
Índice
Sumário .....................................................................................................................
2
Índice .........................................................................................................................
3
1. Introdução ............................................................................................................
7
1.1. Apresentação .............................................................................................
7
1.2. Organização ...............................................................................................
8
1.3. Agradecimentos .........................................................................................
9
1.4. Orientação.................................................................................................. 10
2. Programação em Lógica .......................................................................................
2.1. Origens e características da programação em lógica ................................
2.2. Um pouco da história das implementações de Prolog ..............................
2.3. A WAM .......................................................................................................
2.3.1. Representação dos termos ..............................................................
2.3.2. Áreas de dados .................................................................................
2.3.3. Estruturas de dados de controlo .....................................................
2.3.4. Registos da WAM .............................................................................
2.3.5. Classificação das variáveis ............................................................
2.3.6. Optimização da última chamada (LCO) e trimming .......................
2.3.7. Indexação ........................................................................................
2.3.8. Instruções ........................................................................................
11
11
13
15
16
16
17
17
18
18
19
19
3. Programação em Lógica Contextual ....................................................................
3.1. Introdução .................................................................................................
3.2. Sintaxe .......................................................................................................
3.3. Units ..........................................................................................................
3.4. Designador de Unit ....................................................................................
3.5. Contexto .....................................................................................................
22
22
23
23
24
24
Índice
3.6.
3.7.
3.8.
3.9.
4
Nomes dependentes do contexto ................................................................
Contexto histórico .....................................................................................
Regras de inferência ..................................................................................
Exemplos ...................................................................................................
25
25
25
28
- Máquina abstracta para CxLP ..................................................................
Introdução .................................................................................................
Representação dos termos .........................................................................
Organização da memória ..........................................................................
4.3.1. Área de código .................................................................................
4.3.2. Stacks (heap + stack local) ..............................................................
4.3.3. Trail ................................................................................................
Ligações em CxLP .......................................................................................
4.4.1. Ligação dos nomes definidos localmente .......................................
4.4.2. Ligação dos nomes dependentes do contexto ..................................
4.4.2.1. Cache .................................................................................
4.4.2.2. Estrutura da cache ............................................................
4.4.3. Ligação dos nomes em fórmulas de extensão .................................
4.4.4. Ligação dos nomes importados ......................................................
Parâmetros das units ................................................................................
Implementação dos contextos ...................................................................
4.6.1. Representação dos contextos ..........................................................
4.6.2. Registos para suporte dos contextos ...............................................
4.6.3. Instruções para controlo dos contextos .........................................
4.6.4. Fórmulas de libertação de contexto e a unit [] ................................
Implementação do contexto histórico ......................................................
4.7.1. Representação do contexto histórico e registos associados ...........
4.7.2. Instruções para controlo do contexto histórico .............................
As outras estruturas de dados do stack local ............................................
4.8.1. Registos de activação ......................................................................
4.8.2. Pontos de escolha ............................................................................
A C-WAM ....................................................................................................
30
30
31
32
32
32
33
33
33
34
34
35
35
37
37
38
38
39
41
42
43
43
44
45
45
46
46
5. Mecanismos de ligação incremental da CxM ......................................................
5.1. Introdução .................................................................................................
5.2. O sistema de suporte de CxLP .....................................................................
5.3. Manutenção das ligações na CxM .............................................................
5.3.1. Descritores de nome de predicado ..................................................
5.3.2. Estabelecimento das ligações .........................................................
5.3.2.1. MakeIndex ........................................................................
5.3.2.2. MakeImportBinding ........................................................
5.3.2.3. MakeCtxExtBinding .........................................................
5.3.2.4. MakeCtxDepBinding ........................................................
5.3.3. Invalidação das ligações .................................................................
5.3.3.1. Efeitos da alteração do conjunto de cláusulas de uma unit ......
5.3.3.2. Efeitos da alteração de importação ..................................
5.3.3.3. Efeitos da mudança de visibilidade .................................
5.3.3.4. Efeitos da eliminação de um nome de predicado .............
5.3.4. As ligações dos nomes dependentes do contexto revisitadas .........
5.3.4.1. Adição de cláusulas ..........................................................
47
47
48
50
50
51
52
52
52
52
53
54
55
55
56
56
56
4. CxM
4.1.
4.2.
4.3.
4.4.
4.5.
4.6.
4.7.
4.8.
4.9.
Índice
5
5.3.4.2. Mudança do estado de visibilidade de um nome ..............
5.3.4.3. Eficiência ..........................................................................
57
58
6. Técnicas de implementação da CxM ...................................................................
6.1. Introdução .................................................................................................
6.2. Lips! ............................................................................................................
6.3. Técnica de interpretação ...........................................................................
6.3.1. Código threaded ..............................................................................
6.3.2. Método clássico ...............................................................................
6.3.3. Comparação ....................................................................................
6.3.4. Eliminação do ciclo de interpretação ............................................
6.3.5. Código expandido ............................................................................
6.4. Tags ............................................................................................................
6.5. Rotina de unificação ..................................................................................
6.6. Especialização das instruções ...................................................................
6.7. Utilização de registos da máquina hospedeira .........................................
6.8. Indexação ...................................................................................................
6.9. Penalizações provocadas pelos novos mecanismos .................................
59
59
59
60
60
61
61
62
62
63
66
67
68
69
69
7. Compilação ..........................................................................................................
7.1. Introdução .................................................................................................
7.2. Características gerais do compilador .......................................................
7.3. Funcionamento geral e código gerado .......................................................
7.4. A tabela de variáveis .................................................................................
7.5. Algoritmo de atribuição de registos ..........................................................
7.6. Golos que mudam de contexto ...................................................................
7.6.1. Passagem de parâmetros para units ...............................................
7.6.2. Tratamento de referências a parâmetros de units .........................
7.7. Indexação ...................................................................................................
7.7.1. Índice ...............................................................................................
7.7.2. Índice rápido ...................................................................................
7.7.3. Índice compacto ..............................................................................
7.7.4. Índice super-rápido .........................................................................
7.7.5. Na WAM ...........................................................................................
7.8. Compilador optimizador ..........................................................................
7.9. Outras linguagens ......................................................................................
71
71
72
72
75
76
79
79
80
81
81
82
83
84
85
85
86
8. Outras Componentes do Sistema .........................................................................
8.1. Predicados de sistema ...............................................................................
8.1.1. Predicados deterministas escritos em C ........................................
8.1.2. Predicados de sistema não deterministas escritos em C ...............
8.1.3. Predicados de sistema escritos em CxLP ........................................
8.2. Analisador sintáctico ...............................................................................
8.3. Tratamento de erros ..................................................................................
8.4. Colector de lixo do heap .............................................................................
8.5. Colector de lixo da área de código ..............................................................
8.6. Monitor de Comandos ...............................................................................
87
87
88
88
90
90
91
91
93
93
9. Conclusão .............................................................................................................
94
Índice
6
Apêndices ...................................................................................................................
96
A. Predicados de sistema ..........................................................................................
A.1. Introdução .................................................................................................
A.2. Sucesso e falhanço .....................................................................................
A.3. Controlo .....................................................................................................
A.4. Manipulação de golos ................................................................................
A.5. Gestão das units e do seu conteúdo ............................................................
A.6. Contextos ...................................................................................................
A.7. Classificação de termos .............................................................................
A.8. Manipulação de estruturas ........................................................................
A.9. Igualdade ....................................................................................................
A.10. Avaliação de expressões aritméticas e comparação de números .............
A.11. Gestão de ficheiros .....................................................................................
A.12. Entradas e Saídas ......................................................................................
A.13. Diversos .....................................................................................................
97
97
97
97
98
99
102
103
103
103
104
104
104
106
B. Operações Básicas e Instruções da CxM ...............................................................
B.1. Introdução .................................................................................................
B.2. Pseudo-código ............................................................................................
B.3. Operações básicas da CxM .........................................................................
B.4. Instruções procedimentais e de controlo ..................................................
B.5. Instruções Put ............................................................................................
B.6. Instruções Get ............................................................................................
B.7. Instruções Unify ........................................................................................
B.8. Instruções Build .........................................................................................
B.9. Instruções de indexação ............................................................................
B.10. Instruções para fórmulas de extensão ......................................................
B.11. Instruções de importação ..........................................................................
B.12. Instruções para nomes dependentes do contexto ......................................
B.13. Instruções de gestão do contexto histórico ...............................................
109
109
109
110
114
117
118
119
122
123
125
127
129
130
C. Exemplos de código gerado .................................................................................. 132
Bibliografia ............................................................................................................... 135
7
1
Introdução
1.1. Apresentação
A programação em lógica contextual - ou abreviadamente CxLP - representa um
novo e prometedor esquema de programação, que enriquece de forma significa e útil o
paradigma da programação em lógica. As novidades incluem um suporte para a
estruturação dos programas em diferentes units, e o fornecimento de mecanismos
para combinação dinâmica de units em contextos, que permitem uma forma de
controlo abstracto sobre as derivações dos programas. Este esquema responde a
diversas necessidades sentidas em Inteligência Artificial, sendo por isso de admitir
que ele possa contribuir para o incremento do uso da lógica como formalismo para o
desenvolvimento de aplicações naquela área.
A demonstração da viabilidade de implementar eficientemente uma linguagem
baseada neste paradigma, a um nível próximo dos melhores compiladores de Prolog,
seria com certeza um factor ampliador do interesse por ela. Um dos principais
objectivos do presente trabalho é, precisamente, o de mostrar como isto pode ser feito
numa máquina sequencial convencional, por aplicação de algumas das técnicas que
correspondem ao presente estado da arte da implementação do Prolog. Para isso foi
definida uma máquina abstracta - chamada CxM - que foi concebida como uma
extensão da, muito justamente aclamada, WAM de David Warren. Foram ainda
adaptadas algumas ideias da máquina abstracta C-WAM descrita em [Lamma 89].
Mas o trabalho ficaria incompleto se não incluisse, igualmente, a definição de um
sistema de suporte para CxLP, que fosse encarregado das tarefas de gestão dos dados,
1. Introdução
8
compilação e ligação, e suportando a um ambiente de trabalho mínimo. As duas ideias
chave, que presidiram ao desenvolvimento deste sistema, foram a velocidade de
resposta e flexibilidade na utilização. O aspecto da velocidade foi tratado, não só ao
nível da máquina abstracta (a qual foi alvo de todas as optimizações que foi possível
inventar), mas também ao nível do desempenho do compilador, no qual procurámos
encontrar o melhor compromisso entre rapidez de compilação e qualidade do código
gerado. Quanto à flexibilidade de utilização, foram tratadas entre outras questões, as
que são colocadas em CxLP pelo carácter modular dos programas. Fomos um pouco
além dos mecanismos clássicos da compilação separada e ligação, recorrendo a
técnicas
de
compilação
e
ligação
incrementais,
que
proporcionaram
uma
flexibilidade geral de utilização idêntica à de um interpretador.
1.2. Organização
O próximo capítulo, o segundo deste trabalho, tem um carácter introdutório. Nele é
efectuada uma apresentação sumária das origens e de algumas características
fundamentais
da
programação
em
lógica,
assim
como
da
história
das
implementações da linguagem Prolog. Na sua parte final, é feita uma descrição dos
principais conceitos e nomenclatura envolvidos na máquina abstracta WAM.
No terceiro capítulo descrevemos de forma razoavelmente precisa um sistema
prático para programação em lógica contextual, designado como linguagem CxLP.
Fazemos uma breve descrição das principais noções básicas nele envolvidas, além da
sua definição de um ponto de vista operacional. No final são apresentados alguns
exemplos de utilização da linguagem. Parte deste capítulo constitui uma adaptação de
[Monteiro 89c].
No quarto capítulo são apresentadas e justificadas as principais características da
máquina abstracta CxM com relevo para: forma de representação e gestão dos
contextos, política de estabelecimento de ligações entre nomes e definições de
predicados, e mecanismos para suporte de units parameterizadas.
A forma de conciliação da flexibilidade de um interpretador com a utilização de
técnicas de compilação, é o tema do quinto capítulo. A solução reside nas
características incrementais do sistema que assentam fortemente nos mecanismos de
ligação usados.
No sexto capítulo são abordadas as técnicas de implementação da máquina
abstracta que estão na origem da sua apreciável velocidade. O nosso protótipo de
CxLP, numa forma (quase) portável, corre a aproximadamente 60 Klips numa
VAXstation 3540, o que representa cerca de 2/3 da velocidade do Quintus-Prolog v2.4,
e quase o dobro da velocidade do SB-Prolog e do SICStus-Prolog.
A compilação de programas CxLP constitui o tema do sétimo capítulo. São
referidas as características gerais do compilador, faz-se a descrição de alguns
1. Introdução
9
algoritmos importantes, e discutem-se alguns problemas que surgem na geração de
código específico para controlo dos contextos. São ainda abordadas com algum
detalhe as diversas formas de indexação usadas na CxM.
No capítulo oitavo ocupamo-nos, de forma abreviada, dos seguintes tópicos:
técnicas usadas na implementação dos predicados de sistema, analisador sintáctico
(parser), mecanismo de recuperação de erros de sistema, colectores de lixo e monitor
de comandos.
Na conclusão tecemos breves comentários sobre o estado actual do projecto e o
trabalho a desenvolver a curto prazo.
Antes da bibliografia encontram-se ainda três apêndices onde apresentamos
sucessivamente: a descrição funcional dos predicados de sistema que definimos
(apêndice A), a descrição algorítmica das instruções da CxM com indicação das
condições de geração pelo compilador (apêndice B) e alguns exemplo de código gerado
(apêndice C).
1.3. Agradecimentos
Agradecimentos são devidos a diversas pessoas, a maior parte das quais colegas da
Universidade Nova de Lisboa, pela ajuda prestada no âmbito deste trabalho.
Ao meu orientador Luís Monteiro, e ao meu co-orientador Pedro Veiga, pelo apoio
recebido.
Mais uma vez ao Luís Monteiro, e ao António Porto, pela criação do paradigma da
Programação em Lógica Contextual, o qual constituiu um óptimo pretexto para a
realização deste trabalho.
Ao José Alegria, por inúmeras úteis sugestões e discussões. Reparo agora que a
opção pela realização deste trabalho, bem assim como muitas das suas ideias mais
essenciais (como por exemplo a preocupação com o ambiente de programação),
resultaram muito da sua influência e incentivo ao longo dos últimos anos de trabalho
conjunto.
Ao Paulo Rosado, pelas discussões mantidas sobre a WAM, que muito me ajudaram
a atingir o seu entendimento completo.
Ao Salvador Abreu, pela preciosa ajuda prestada no apuramento, em termos de
velocidade de execução, da versão do sistema CxLP, que corre nos Vax.
À Margarida Mamede, pelo atendimento de inúmeras dúvidas de amplo espectro,
desde a Programação em Lógica até às regras gramaticais da Língua Portuguesa.
Ao Vasco Pedro, pelo esclarecimento de dúvidas relacionadas com a CxLP e com o
pré-processador que ele realizou para uma versão desta linguagem.
1. Introdução
10
Ao Gabriel David, por me ter mostrado a grande utilidade prática de um mecanismo
do tipo do contexto histórico em CxLP, o qual estava ausente dos planos iniciais .
Ao Carlos Ponte e ao António Páscoa por úteis discussões relacionadas com a
implementação da CxM, que contribuíram principalmente para o estabelecimento do
formato final das tags dos termos da linguagem CxLP.
Ao meu pai, Artur Vieira Dias, pela leitura e correcção a nível gramatical de uma
versão deste trabalho.
1.4. Orientação
Esta tese foi orientada por Luís Monteiro, Professor Associado da Universidade
Nova de Lisboa, e co-orientada por Pedro Veiga, Professor Associado da Universidade
Técnica de Lisboa. Foi realizada entre os meses de Novembro de 1989 e de Julho de
1990.
11
2
Programação em lógica
2.1. Origens e características da programação em lógica
A programação em lógica surgiu na sequência de importantes desenvolvimentos na
área da demonstração automática de teoremas, com destaque para a descoberta do
princípio da resolução em 1965 por Robison [Robison 65]. Este princípio consiste num
processo sistemático e eficiente, que permite efectuar demonstrações orientadas por
objectivos em lógica de predicados de primeira ordem e que usa, na representação das
proposições lógicas, a chamada forma clausal.
A ideia de olhar para conjuntos de proposições lógicas como programas e de
considerar o processo de raciocínio, o qual envolve longas sequências de inferências
focalizadas para um objectivo, como computação, deve-se a Green [Green 68].
Em 1972, Alain Colmeraurer, com a ajuda de P. Roussel, desenvolveu na
Universidade de Marselha um sistema de raciocínio destinado ao processamento de
linguagem natural, no qual mostrou que se fosse considerada uma parte restrita da
lógica
proposicional,
o
princípio
da
resolução
podia
ser
implementado
eficientemente, de forma análoga à da invocação de procedimentos nas linguagens
imperativas [Colmeraurer 72]. Este sistema, escrito em Algol W, foi chamado de
Prolog, nascendo assim esta linguagem.
Uma implementação mais prática do Prolog seria desenvolvida no ano seguinte
por dois discípulos de Colmeraurer: Battani e Meloni [Battani 73]. Foi usada na
implementação a linguagem Fortran, a única suficientemente portável da época,
tendo-se registado uma considerável difusão do sistema. A influência deste Prolog,
2. Programação em Lógica
12
dito de Marselha, persiste ainda nos dias de hoje, atendendo a que algumas das suas
características mais importantes são ainda aceites na prática, estando presentes na
maior parte das implementações actuais da linguagem. Os aspectos a que nos
referimos são os seguintes: estratégia de controlo, utilização dos predicados de
sistema cut/0, univ/2, assert/1 e retract/1, possibilidade de definição de operadores
pelo utilizador, e suporte para gramáticas de cláusulas definidas ([Colmeraurer 75]).
Em 1974, Kowalsky e van Emden identificaram a natureza da restrição usada por
Colmeraurer no Prolog, e formularam com rigor uma interpretação procedimental
para o domínio da lógica das cláusulas de Horn, baseada na estratégia de controlo
SLD-resolução [Kowalski 74]. A estratégia usada na linguagem Prolog - pesquisa
depth-first com retrocesso, no espaço de estados - representa apenas um caso
particular deste tipo de resolução: outra alternativa, que neste caso tem a
desvantagem de ser mais exigente em termos de recursos computacionais, é a
correspondente a uma pesquisa breadth-first, tal como se faz na linguagem LogLisp
[Robison 80].
É também a van Emden e Kowalsky que devemos outro estudo fundamental da
semântica da programação em lógica [van
Emden
76].
Neste
trabalho
eles
desenvolveram três tipos distintos de semânticas - operacional, declarativa (ou
não -operacional) e denotacional - e provaram a sua equivalência.
Desde as suas origens, que acabámos de descrever, até aos dias de hoje, muito
trabalho tem sido desenvolvido na área da programação em lógica, registando-se
grande evolução. Do ponto vista da sua aplicação prática, o paradigma tem tido
também bastante sucesso e ganho numerosos aderentes, o que não será de surpreender
se pensarmos nas suas múltiplas vantagens como ferramenta de programação:
expressividade, clareza, versatilidade, facilidade de manutenção, vasto domínio de
aplicação, etc.
O aspecto crucial deste tipo de programação, de onde derivam todas as vantagens
atrás referidas, reside no seu forte carácter declarativo, herdado da utilização da
lógica como formalismo para a escrita de programas. É aliás este o motivo que
justifica o facto das linguagens lógicas serem muitas vezes classificadas como sendo
de muito alto nível. Com efeito, é frequente que uma boa implementação de um
programa em lógica seja também uma sua boa especificação.
Este aspecto declarativo é realçado pela circunstância de o conteúdo descritivo dos
programas em lógica ser absolutamente independente dos mecanismos de execução
usados, ao contrário do que se passa com as linguagens imperativas. Inclusivamente,
o comportamento das suas variáveis é controlado pelo processo de alto nível do
emparelhamento de padrões da unificação, não necessitando de ser analisado por
referência a um estado da computação.
2. Programação em Lógica
13
Apesar do que ficou dito, durante o desenvolvimento de programas Prolog não é
possível evitar ter de considerar o controlo da sua execução, se quisermos que sejam
eficientes e que existam garantias de terminação. Idealmente, seria excelente que o
sistema de suporte da linguagem tivesse a possibilidade de deduzir a partir da
componente declarativa de um programa uma estratégia de controlo adequada;
contudo parece difícil que esta esta ideia possa alguma vez deixar de fazer parte do
domínio da ficção científica.
2.2. Um pouco da história das implementações de Prolog
Antes que a lógica pudesse ser considerada como um formalismo prático,
suficientemente competitivo para o desenvolvimento de programas, foi necessária
uma grande evolução na tecnologia de implementação.
Pelos padrões actuais, o interpretador de Prolog de Marselha não era muito
eficiente, quer em termos de utilização de memória, quer em termos de velocidade de
execução. Por exemplo, os registos de activação criados na invocação dos predicados
só eram desempilhados na ocorrência de retrocesso e, no que respeita à velocidade,
segundo Warren [Warren 74] ela não excedia os 200 lips num IBM 360-67. Foi nesta
implementação que pela primeira vez foi usada a importante a técnica de
representação dos termos chamada de partilha de estrutura ou SS (structure sharing),
na qual se tira partido do facto de diferentes instâncias do mesmo termo diferirem
apenas nas instanciações das suas variáveis.
Bruynooghe desde muito cedo se interessou pelos problemas de gestão de memória
envolvidos no Prolog. A ele se deve um interpretador [Bruynooghe 76] onde pela
primeira vez foi usado um algoritmo de recuperação de memória na pilha de execução
e foi
implementada
a
optimização
TRO (tail
recursion
optimization). Esta
optimização permite substituir certas recursões por iterações, que assim são
executadas sem consumo crescente de memória. Subsequentemente usou num
interpretador escrito em C, datando de 1979, uma técnica de representação dos termos
dita NSS (non-structure sharing) baseada em cópia de estrutura.
A primeira experiência de aplicação de técnicas de compilação à implementação da
linguagem Prolog é devida a David Warren e data de 1977 [Warren 77a]. Nessa
implementação, que deu origem ao conhecido “Dec-10 Prolog”, era gerado código
nativo sendo usada a técnica de partilha de estrutura na representação dos termos.
Entre outras novidades, Warren introduziu a noção de variável local e de variável
global, pilhas de execução múltiplas para permitir recuperação de memória nas fases
deterministas da execução dos programas, e um mecanismo de indexação das
cláusulas pelo primeiro símbolo funcional. O ganho de velocidade conseguido em
relação aos interpretadores de Prolog foi da ordem de 5 a 10 vezes mais. Este ganho é
bastante inferior (cerca de 3 vezes) ao que geralmente se consegue obter na compilação
2. Programação em Lógica
14
das linguagens imperativas tradicionais e justifica-se, em parte, pelo facto de o nível
de pré-tratamento possível, ser condicionado pela ocorrência de variáveis nos
objectos manipulados. Esta circunstância impõe na prática um certo grau de
interpretação durante a execução de programas. De qualquer maneira, a melhoria de
desempenho foi significativa e teve grande importância teórica pois pela primeira
vez, foi demonstrado que a linguagem podia rivalizar em termos de desempenho com a
sua rival natural na área do processamento simbólico: a linguagem Lisp (ver
[Warren 77b]).
Um outro marco fundamental na evolução das implementações do Prolog foi
também protagonizado por D. Warren em 1983 [Warren 83], quando apresentou a
máquina abstracta WAM (Warren abstract machine) que se destinava a ser
implementada por meio de emulação (em software , firmware ou hardware). Esta
incluía significativas melhorias em relação à versão de 1977. As técnicas usadas
incluíam uma representação de termos NSS, separação entre registos de activação e
pontos de escolha, um novo método de optimização chamada LCO (last call
optimization) que generalizava a técnica TRO, e a introdução da noção de
registo-máquina para incrementar as oportunidades de optimização do código. Nesta
máquina era feita uma admirável compatibilização e síntese dos principais
desenvolvimentos que até à altura tinham resultado
de
muita
discussão
e
investigação.
A WAM está na base de algumas das melhores implementações actuais do Prolog, e
só não tem tido maior divulgação devido à sua relativa complexidade aliada ao
hermetismo da escassa informação disponível. Só muito recentemente, surgiu uma
monografia na qual a WAM é finalmente tornada mais acessível: [Aït-Kaci 90].
Durante os anos 80 floresceram implementações de Prolog abrangendo uma gama
vasta de filosofias. Eis exemplos de algumas técnicas de implementação retiradas da
referência [Hermenegildo 89]:
• Compilação para código da WAM ou de evoluções desta: SICStus-Prolog,
SB-Prolog, Quintus-Prolog
• Compilação para código nativo: BIM-Prolog
• Tradução directa para C: Proteus-Prolog
• Concepção de hardware especializado, em muitos casos derivado da WAM: PSI-I,
PSI-II, PLM, IPP, CHI -I, CHI-II, Low-RISC.
Pesando as vantagens e inconvenientes destas aproximações em relação a diversos
factores conflituantes - portabilidade, compacidade do código gerado, velocidade de
execução dos programas, grau de complexidade da implementação,
optimização do código
possível,
facilidade
de
integração
num
nível
de
ambiente
de
programação, adequação à prototipagem e integração de novas ideias, custo
2. Programação em Lógica
15
monetário - parece que na maioria dos casos a melhor será aquela que corresponde à
primeira aproximação, não obstante em termos de velocidade pura, as outras três,
com destaque para a última, deverem merecer preferência.
Para além das evoluções registadas na implementação dos mecanismos de
execução do Prolog, a tecnologia de compilação tem também
sido
alvo
de
significativos desenvolvimentos, sendo actualmente usadas sofisticadas técnicas de
análise global de programas entre as quais de destaca um método geral chamado de
interpretação abstracta [Cousot 77]. Mediante a utilização de diferentes domínios
abstractos, ele permite extrair dos programas informação de natureza diversa,
alguma da qual pode ser usada na optimização de código. É por exemplo exemplo
possível inferir uma grande parte dos modos (entrada, saída ou misto) dos parâmetros
formais dos predicados. Daqui resulta a viabilidade da descoberta do carácter
determinista ou funcional de alguns predicados, e a possibilidade de se fazer um
pré-tratamento parcial de maior número de unificações.
Estão ainda na ordem do dia as implementações de linguagem lógicas em
máquinas paralelas, tanto em arquitecturas multiprocessador de utilização geral
como em arquitecturas especialmente adaptadas. Foi Kowalski quem pela primeira
vez referiu o potencial da programação em lógica na programação de máquinas
paralelas, que advém do facto de o conteúdo declarativo de um programa em lógica ser
independente da estratégia de execução usada [Kowalski 79b]. De modo geral, nas
linguagens procedimentais clássicas, até mesmo o conteúdo lógico dos seus programas
está demasiado comprometido com o modelo clássico de computação de von Newman.
2.3. A WAM
A WAM tem sido na prática reconhecida como um standard para a implementação
de compiladores de Prolog, encontrando-se na base das melhores implementações
actuais da linguagem. Tem sido, além disso, utilizada em algumas importantes áreas
de investigação tais como a demonstração automática de teoremas, o desenho de
hardware especializado para execução altamente eficiente de Prolog e como suporte de
extensões ao paradigma lógico (com destaque para a concorrência). Este trabalho
representa mais um exemplo de aplicação da WAM.
Apesar da WAM estar quase a fazer sete anos, devido a razões diversas o
conhecimento íntimo dos seus mecanismos não tem atingido a divulgação que lhe
faria justiça. Para tornar este trabalho minimamente acessível a um leitor não
familiarizado com ela, pareceu-nos indispensável fazer uma breve apresentação das
suas características mais importantes, o que também servirá de pretexto para a
introdução de alguma terminologia necessária. Somos obrigados no entanto de
ignorar, entre outros aspectos, os intrincados detalhes de baixo nível e a justificação
da configuração da máquina, cujo desenho final resultou de diversas optimizações
2. Programação em Lógica
16
subtis e intrinsecamente ligadas. Para este estudo aprofundado da WAM recomenda-se
a referência, [Aït-Kaci 90].
2.3.1. Representação dos termos
Um termo é representado na WAM por uma célula de memória na qual se
distinguem essencialmente duas zonas: a tag e o valor. A tag serve para distinguir o
tipo do termo o qual pode ser variável, estrutura, lista ou constante. Quanto ao valor
ele constitui uma referência para a memória, excepto no caso das constantes inteiras e
reais em que consiste num valor numérico.
Uma variável livre é representada, por convenção, como uma auto-referência,
enquanto que uma variável instanciada contém o termo que lhe foi atribuído. Daqui
resulta que um conjunto de variáveis ligadas acaba por ser representado por uma lista
ligada.
A forma de representação dos termos usada é a designada de cópia de estrutura, que
se caracteriza pelo facto de a criação de um termo estruturado exigir a cópia explicita
do seu functor e argumentos para posições de memória consecutivas.
2.3.2. Áreas de dados
São usadas na WAM cinco áreas de dados distintas, as quais são seguidamente
apresentadas:
•Área de código
Contém o programa.
•Stack local
Centraliza toda a informação de controlo respeitante
aos
mecanismos de activação de predicados e de retrocesso. Os dois
tipos de estruturas dados usadas nestas tarefas, chamam-se
respectivamente registos de activação e pontos de escolha.
•Heap
Também conhecido por stack global, é constituído pelos termos
criados durante a execução do programa, como resultado das
unificações e da passagem de termos estruturados nas chamadas
de predicados.
•Trail
Pilha onde são guardadas referências para todas as variáveis
instanciadas condicionalmente, para que sejam reinicializadas
quando ocorrer retrocesso.
•PDL
Pilha usada pela rotina de unificação geral.
Os dados que se encontram no stack local e no heap obedecem a diversos
invariantes que garantem a coerência do estado interno da máquina. Estas duas áreas
crescem geralmente na invocação de predicados, e reduzem-se quando ocorre
retrocesso. O stack local contrai-se também sempre que termina, de forma
determinista a execução de um predicado.
2. Programação em Lógica
17
2.3.3. Estruturas de dados de controlo
A activação de uma cláusula implica muitas vezes a criação de um registo de
activação. Este é constituído por um vector, onde são guardadas as suas variáveis
permanentes, um apontador (chamado continuação) que indica o golo de outra
cláusula onde a execução irá prosseguir quando a cláusula corrente terminar, e ainda
outro apontador para o registo de activação da cláusula de continuação. Estes dois
apontadores representam a lista dos golos que aguardam execução, ou seja, no fundo, a
resolvente corrente do sistema de inferência.
A decisão de permitir a existência de variáveis no stack local destina-se a poupar
espaço no heap, já que neste, os dados são bastante persistentes. Introduz no entanto,
no tratamento das variáveis livre, uma pequena complicação, relacionada com a
manutenção dos invariantes da máquina, a qual tem de ser gerida parcialmente em
tempo de compilação e parcialmente em tempo de execução.
Um ponto de escolha é criado sempre que é chamado um predicado não
determinista, no momento exacto que precede a activação da primeira cláusula que
responde à chamada. Representa um instantâneo do estado da computação no
momento da invocação, servindo para restaurar esse estado em caso de retrocesso,
para ser tentada a próxima cláusula. Só é destruído no momento em que não restarem
cláusulas alternativas. O conteúdo dos pontos de escolha é o seguinte: valores dos
argumentos da chamada (têm de ser repostos em cada nova tentativa de satisfação da
chamada), conteúdo dos principais registos da WAM (H, TR, B, CP, E), e referência para
o código da próxima cláusula alternativa.
2.3.4. Registos da WAM
O conjunto de registos da WAM definem o estado corrente da computação. São os
seguintes:
•P
Contador de programa, aponta para a instrução corrente na zona de código.
• CP Continuação de programa, é carregado com o endereço de retorno da chamada
de um predicado, desde que ela não seja a última da cláusula.
•E
Registo de activação corrente.
•B
Ponto de escolha corrente.
• TR Topo do trail.
•H
Topo do heap.
• HB Valor do registo H no ponto de escolha corrente. Permite acelerar o teste de
averiguação da necessidade de memorização de uma variável no trail, o qual é
feito sempre que se instancia uma variável.
2. Programação em Lógica
•S
18
Chamado apontador de estrutura. Trata-se de um apontador para o heap que
desempenha um papel central nas unificações que se processam durante o
emparelhamento de uma cláusula com uma chamada.
• Xi
Conjunto de registos usados para passagem de parâmetros
e
para
armazenamento temporário de valores. Muitas das optimizações que é
possível introduzir no código gerado, baseiam-se no uso destes registos.
Note-se que os registos da WAM são, como a própria máquina, virtuais, sendo
geralmente definidos, na linguagem de implementação, como um simples
vector de células de memória.
2.3.5. Classificação das variáveis
Em relação às variáveis que surgem nas cláusulas do programa fonte, Warren fez
uma distinção entre variáveis permanentes e variáveis temporárias. Simplificando
um pouco a realidade, uma variável diz-se permanente quando o seu conteúdo tiver de
ser preservado ao longo da derivação de vários golos dentro do corpo de uma cláusula.
São armazenadas nos registos de activação das cláusulas a que pertencem, sendo
denotadas por Y i, em que i indica o seu deslocamento em relação à base do registo de
activação. As restantes variáveis, ditas temporárias, são associadas a registos Xi da
WAM, o que é vantajoso em termos de velocidade e de ocupação de memória1.
Importantes optimizações podem ser feitas pelo compilador, através da criteriosa
escolha dos registos a associar às variáveis temporárias.
2.3.6. Optimização da última chamada (LCO) e trimming
Numa cláusula, é perfeitamente natural a circunstância de uma variável
permanente ser referida, por exemplo, no seu último golo. Este facto, à primeira vista,
sugere a necessidade de deixar a eliminação do registo de activação da cláusula só para
depois de ter sido executado o seu último golo, já que caso contrário, algumas
variáveis permanentes deixariam de ter memória associada, o que aparenta ser
catastrófico.
Apesar de tudo, na WAM os registos de activação são por regra desempilhados no
momento que precede a chamada do último golo das cláusulas, já depois dos
argumentos actuais terem sido carregados nos registos Xi . O problema descrito no
parágrafo anterior coloca-se de facto, mas felizmente apenas num caso particular, que
pode ser facilmente antecipado pelo compilador. Trata-se do caso em que, em tempo de
execução, uma variável permanente, ocorrendo no último golo de uma cláusula ao
nível da superfície, se encontra livre ou ligada a uma variável qualquer do registo de
activação prestes a desaparecer. As variáveis assinaladas pelo compilador como
1O valor dos registos X i não é preservado nas invocações de predicados, razão que os impede
de funcionar como variáveis permanentes.
2. Programação em Lógica
19
suspeitas, são designadas como variáveis unsafe, sendo gerados padrões especiais de
código para ela. Em tempo de execução, quando uma variável unsafe é passada como
parâmetro actual da última chamada de uma cláusula, testa-se se ela refere de alguma
forma o registo de activação a ser destruído. Em caso positivo procede-se à sua
transferência para o heap, o que envolve algumas manipulações cujos detalhes não
são aqui explicados.
Esta técnica tem consequências extremamente importantes: redução geral de
consumo de memória no stack local, execução de predicados recursivos deterministas
sem crescimento do stack local, e ausência de necessidade de criação de registos de
activação nas cláusulas com menos de dois golos no corpo. Desta menor frequência na
criação de registos de activação resulta ainda um significativo aumento da velocidade
da máquina.
Na WAM a técnica de optimização da última chamada (LCO), atrás descrita, é ainda
objecto de uma generalização denominada trimming. O trimming consiste na
progressiva redução de tamanho do registo de activação de uma cláusula à medida que
se vai fazendo a sua execução da esquerda para a direita, por libertação do espaço
ocupado pelas variáveis permanentes que vão deixando de ser referidas. A eliminação
destas variáveis levanta problemas semelhantes aos que surgem na aplicação de LCO,
pelo que a noção de variável unsafe e concomitante técnica de transferência para o
heap, são generalizadas aos golos intermédios das cláusulas. O uso de trimming
implica que o compilador faça uma adequada ordenação das variáveis dentro do
registo de activação por forma a maximizar a poupança de memória obtida. O número
de variáveis permanentes a preservar em cada golo do corpo de uma cláusula, é
registado como segundo argumento nas instruções Call Proc, N.
2.3.7. Indexação
Warren reintroduziu na WAM o mecanismo de indexação das cláusulas pelo
primeiro símbolo funcional, que já tinha usado no seu primeiro compilador em 1977.
Trata-se de um método que permite fazer uma filtragem das cláusulas de um predicado
candidatas a emparelhamento, após cada
chamada.
O
ganho
de
velocidade
proporcionado é geralmente muito significativo, já que se evita a exploração de
muitas cláusulas infrutíferas. O caso mais favorável é aquele em que o resultado da
filtragem se reduz a uma única cláusula: o predicado torna-se determinista não sendo
necessário criar qualquer ponto de escolha.
2.3.8. Instruções
Até este momento já descrevemos uma parte substancial da funcionalidade da
WAM; não apresentámos ainda, no entanto, qualquer indicação sobre a natureza das
suas instruções.
2. Programação em Lógica
20
A ideia que se encontra na base da definição de um conjunto de instruções
abstractas para suporte da funcionalidade da WAM é simples: consiste em identificar
todas as operações elementares significativas, procedendo de seguida ao seu
isolamento em instruções máquina específicas. O conjunto básico de instruções da
WAM, de acordo com [Warren 83], compreende um número reduzido de cerca de 40
instruções. Os programas Prolog são codificados em sequências destas instruções. Os
seus argumentos, que são em número não superior a quatro, podem representar
diversas entidades: variável temporária (Xi ), variável permanente (Yi ), functor (F),
predicado (Pred), constante (C), etiqueta de programa (L), valor inteiro pequeno (N), ou
tabela de dispersão (HashTable).
As instruções da WAM agrupam-se nas cinco grandes classes, seguidamente
indicadas:
Instruções Procedimentais - Fazem a gestão dos registos de activação e das
transferências de controlo.
Allocate
Deallocate
Proceed
Execute Pred
Call Pred, N
Instruções Put - Carregam os argumentos actuais nos registos Xi , preparando a
invocação dos predicados.
PutVariable Vn, Xi
PutValue Vn, Xi
PutUnsafeValue Yn, Xi
PutConstant C, Xi
PutStructure F, Xi
PutList Xi
Instruções Get - Fazem a unificação dos argumentos formais das cláusulas com os
argumentos actuais passados em registos Xi .
GetVariable Vn, Xi
GetValue Vn, Xi
GetConstant C, Xi
GetStructure F, Xi
GetList Xi
Instruções Unify - Fazem as unificações correspondentes aos argumentos de termos
estruturados. Funcionam em dois modos: leitura e escrita. Em modo de leitura
unificam os argumentos de duas estruturas; em modo de escrita constróiem os
argumentos de uma nova estrutura. A WAM transita para modo de escrita sempre que
se processa a unificação de uma variável com um termo estruturado: recordemos que a
WAM usa a técnica de cópia de estrutura na representação dos termos.
UnifyVariable Vn
UnifyVoid N
UnifyValue Vn
UnifyConstant C, Xi
UnifyStructure F, Xi
UnifyList Xi
2. Programação em Lógica
21
Instruções de indexação - Dividem-se em duas subclasses que incluem as instruções
de pré-selecção das cláusulas candidatas a responder a uma chamada, e as instruções
de exploração das diversas cláusulas alternativas de um predicado (com gestão dos
pontos de escolha).
SwitchOnTerm Lv, Lc, Ll,Ls
SwitchOnConstant N, HashTable
SwitchOnStructure N, HashTable
TryMeElse L
RetryMeElse L
TrustMe
Try L
Retry L
Trust L
No apêndice B podem ser observadas as definições rigorosas das instruções de uma
máquina abstracta - a CxM -, algumas das quais são idênticas às da WAM. Por sua vez,
no apêndice C encontram-se alguns exemplos de codificação de cláusulas usando as
instruções da máquina CxM, exemplos esses que também dão uma ideia aproximada
de como a codificação é feita na WAM.
22
3
Programação em Lógica Contextual
3.1. Introdução
Ao longo dos últimos anos, o paradigma da programação em lógica tem sido objecto
de múltiplas extensões e variações. Existem dois conjuntos de factores justificativos
deste facto.
Em primeiro lugar, não obstante as inúmeras vantagens que as linguagens lógicas
do tipo do Prolog reconhecidamente proporcionam, algumas importantes limitações
têm sido identificadas. De entre as que habitualmente são mais referidas destacamos:
a estratégia de controlo usada, a utilização indiscriminada da negação como
falhanço, a utilização de predicados extra-lógicos para a modificação dinâmica da
base de dados, inadequação para o tratamento de conhecimento não monotónico e a
inexistência de mecanismos de suporte para aplicação de princípios de engenharia de
software no desenvolvimento de programas.
Em segundo lugar, os investigadores têm constatado da conveniência de ampliar o
poder expressivo e a eficiência dessas linguagens, através da introdução de novos
mecanismos que as tornem mais adaptadas a domínios específicos de aplicação e
ainda, procedendo à sua integração com as mais valiosas facilidades oferecidas por
outros paradigmas de programação: concorrência, funções, objectos e restrições.
A proposta de extensão do paradigma de programação em lógica, que está na base
deste trabalho, e que recebeu o nome de Programação em Lógica Contextual - CxLP -, é
devida a Luís Monteiro e António Porto que a apresentaram em 1989 [Monteiro 89a].
As adições efectuadas ao esquema base tiveram dois objectivos em vista.
3. Programação em Lógica Contextual
23
O primeiro foi o de enriquecer o paradigma com mecanismos para suporte da
aplicação de alguns importantes princípios de engenharia de software: modularidade
e generalidade. Para isso introduziu-se o clássico conceito de módulo, que aqui recebe
o nome de unit. O controlo da visibilidade dos predicados no exterior das units, assim
como as importações, são feitas da forma tradicional mediante declarações
apropriadas. As units podem ser parametrizadas sendo geralmente os valores dos seus
parâmetros outras units.
O segundo objectivo têm a ver com estabelecimento de um modelo computacional
para suporte directo de raciocínio contextual, o qual surge de forma constante em
Inteligência Artificial. A execução de um programa em CxLP corresponde à prova de
golos em relação a contextos os quais se definem como simples sequências de units. Os
contextos podem ser alterados dinamicamente de diversas formas, sendo a mais
notável a que recorre ao uso das chamadas fórmulas de extensão. Estas fórmulas
permitem ampliar o contexto de uma prova com as definições de predicados contidas
numa ou mais units.
Neste capítulo é feita uma descrição sumária, mas suficientemente precisa para os
nossos objectivos, das principais noções básicas envolvidas num sistema prático de
programação em lógica contextual - linguagem CxLP -, sendo ainda
incluída a
definição da sua semântica operacional. Descrições mais aprofundadas e detalhadas
poderão ser encontradas em [Monteiro 89a] e [Monteiro 89c]. No final são
apresentados alguns pequenos exemplos de utilização da linguagem.
3.2. Sintaxe
A sintaxe da linguagem CxLP é uma extensão directa da sintaxe da lógica das
cláusulas de Horn. O seu alfabeto é constituído, para além de nomes de variáveis, de
nomes de funções, e de nomes de predicados, também por nomes de unit. Os termos,
fórmulas atómicas e cláusulas da linguagem são definidos da forma habitual, excepto
no que diz respeito a quatro novos tipos de fórmulas que podem ocorrer no corpo das
cláusulas. São as fórmulas de extensão “U>>G”, fórmulas de salvaguarda de contexto
“>G”, fórmulas de reposição de contexto “<G” e fórmulas de libertação de contexto
“?G”. Nestas fórmulas, U pode ser um designador de unit ou uma variável, e G é uma
fórmula qualquer.
3.3. Units
Em programação em lógica contextual um programa consiste num sistema de
units. Cada unit U caracteriza-se pelos seguintes quatro atributos: etiqueta U L,
definições U D , importações U I e visibilidade U V: Vejamos em que consiste cada um
deles:
3. Programação em Lógica Contextual
24
• Etiqueta U L é um termo u(P 1,...,P n ) (n≥0), onde u é o nome da unit e P 1,...,P n são
variáveis distintas, que recebem o nome de parâmetros da unit. Uma
propriedade
importante
dos
nomes
de
units
é
que
estes
determinam
univocamente a unit que nomeiam.
• Definições U D é um conjunto de cláusulas h:-b que se dizem definidas
localmente em U. Nestas
cláusulas,
além
das
variáveis
normais
cuja
quantificação implícita, como se sabe, abrange toda a cláusula, podem aparecer
também parâmetros da unit cuja quantificação abrange toda a unit. O conjunto
de todos os nomes de predicado definidos numa unit U representa-se por
Definidos(U).
• Importações U I é um conjunto de pares <p,t> onde p é um nome de predicado (que
se diz importado) e t é um termo cujas variáveis são todas parâmetros em U L. O
conjunto de todos os nomes de predicado importados por uma unit U
representa-se por Importados(U).
• Visibilidade U V é um conjunto de nomes de predicados que se dizem visíveis no
exterior de U.
Por definição, os conjuntos U D , U I e U V que intervêm da caracterização de U
obedecem às seguintes restrições:
• Um nome de predicado não pode ser simultaneamente importado e definido
localmente em U.
• Um nome de predicado só pode ser mencionado uma vez no conjunto das
importações.
• Só os nomes de predicado definidos localmente ou importados podem aparecer
na visibilidade de U.
3.4. Designador de Unit
Chama-se designador de unit a qualquer instância da etiqueta U L de uma unit U
existente. Desta forma, é condição necessária e suficiente para que um termo t seja
designador de unit que ele corresponda a uma estrutura cujo functor principal seja o
nome de uma unit existente. O predicado designador_de_unit(t) permite testar se um
termo t é um designador de unit. A instância de unit que é determinada por um
designador de unit t chama-se unit designada e representa-se por |t|.
3.5. Contexto
Um contexto define-se simplesmente como uma pilha de designadores de unit e
representa-se pela simples concatenação desses designadores. Assim em relação ao
contexto uC, o designador de unit u corresponde ao topo do contexto sendo C a sua
3. Programação em Lógica Contextual
25
parte restante. A unit designada pelo topo do contexto corrente, neste caso |u|, recebe o
nome de unit corrente. O contexto vazio é representado por λ.
O contexto varia durante a execução dos programas e determina, em cada momento,
o conjunto de cláusulas a serem consideradas na prova dos golos.
3.6. Nomes dependentes do contexto
Todos os nomes de predicado que sejam referidos nos corpos das cláusulas de uma
unit U e aos quais não correspondam nomes de predicados locais ou importados
dizem-se dependentes
do
contexto. Da
definição
operacional
da
linguagem
apresentada mais adiante, extrai-se que a definição de predicado associada a um
nome dependente do contexto, é a que se encontra na unit designada que se encontra
mais próximo do topo do contexto corrente que possui uma definição visível para esse
nome.
3.7. Contexto histórico
O contexto histórico (ou contexto de nível 2), define-se como uma pilha de
contextos. É representado por uma sequência de contextos separados pelo operador
infixo “•”. Desta forma, em relação ao contexto histórico C•Ψ, o contexto C é o seu topo
sendo Ψ a parte restante. O contexto situado no topo do contexto histórico é aquele que
directamente afecta as derivações efectuadas durante a execução dos programas
recebendo por isso o nome de contexto corrente.
O contexto histórico pode ser alterado durante a execução dos programas. Constitui
um meio de memorização de contextos com vista à sua posterior reposição.
3.8. Regras de inferência
Em programação em lógica contextual as derivações são sempre feitas em relação a
contextos. A forma geral de um elemento da relação de derivação é pois a seguinte:
C•Ψ |M g [θ]
em que C é o contexto corrente, Ψ a parte restante do contexto histórico, g um golo, θ
uma substituição e M o modo da derivação. M toma valores no conjunto {i, e} cujos
elementos representam respectivamente os modos interno e externo que têm de ser
considerados para modelar as restrições de visibilidade que limitam o uso das
definições dos predicados.
Na escrita das regras de inferência usamos, tal como em [Monteiro 89c], a forma de
Plotkin [Plotkin 81]:
Antecedentes
Consequente
Condições
3. Programação em Lógica Contextual
26
onde Consequente é um elemento da relação de derivação, Antecedentes é constituído
por zero, um ou dois elementos da relação de derivação e Condições é um conjunto,
eventualmente vazio, de proposições. A interpretação procedimental de uma regra
destas é a seguinte: para estabelecer o Consequente no caso em que se todos os
elementos de Condições são verdadeiros derivam-se os Antecedentes.
Seguidamente apresenta-se a lista
das
regras
de
inferência
que
definem
operacionalmente a linguagem CxLP e que serviu de referência para a implementação
efectuada. Convém referir desde já que todas as regras são mutuamente exclusivas e
que, como na interpretação procedimental da lógica das cláusulas de Horn, o não
determinismo ocorre apenas ao nível da regra Redução.
True
C•Ψ |M true[ε]
O golo true é derivável em qualquer contexto e modo, usando a substituição vazia.
Conjunção
C•Ψ |M g1 [θ]
(C•Ψ)θ |M g2θ [σ]
C•Ψ |M (g1,g2) [θσ]
Para derivar uma conjunção derivam-se os dois golos no mesmo contexto e modo.
Note-se que tanto o contexto corrente como o contexto histórico podem mudar durante
a derivação de g1, pelo que é necessário fazer a sua reposição para a execução de g2.
Além disso atendendo a que C•Ψ pode conter variáveis, as substituições que resultam
da instanciação de variáveis de g1 também lhe devem ser aplicadas para que g2θ seja
correctamente derivado.
Redução
(uC•Ψ)θ |i bθ [σ]
uC•Ψ |i g [θσ]
{ h :- b ∈ |u|D, θ = mgu(g,h) }
Esta regra aplica-se em modo interno ao caso em que o predicado associado ao golo
atómico g está definido na unit corrente |u|. É feita a redução de g na unit corrente e
prossegue-se com a derivação do corpo da clausula usada nessa redução. A expressão
mgu(g,h) representa o unificador mais geral entre g e h.
Substituição
vC•Ψ|e g [θ]
uC•Ψ |i g [θ]
{ <nome(g),v> ∈ |u|I ,
designador_de_unit(v) }
Em modo interno, se o nome de predicado associado a g for importado a partir de
uma unit v, é feita a substituição de u por v no topo do contexto corrente, e a derivação
de g no novo contexto prossegue em modo externo.
3. Programação em Lógica Contextual
27
Saída
C•Ψ |e g [θ]
uC•Ψ |i g [θ]
{ nome(g) ∉ Definidos(|u|) ∪
Importados(|u|) }
Em modo interno, para derivar um golo atómico g sem definição local ou
importada na unit corrente, passa-se para modo externo e elimina-se o topo do
contexto corrente. A derivação de g prossegue no novo contexto.
Descida
C•Ψ |e g [θ]
uC•Ψ |e g [θ]
{ nome(g) ∉ |u|V }
Em modo externo, caso o nome de predicado do golo g não seja visível na unit
corrente, g é derivado também em modo externo mas no contexto que se obtém do
inicial, eliminando o seu topo.
Entrada
uC•Ψ |i g [θ]
uC•Ψ |e g [θ]
{ nome(g) ∈ |u|V }
Em modo externo, caso o nome do predicado de g seja visível na unit corrente, a
derivação prossegue em modo interno no mesmo contexto.
Extensão
uC•Ψ |e g [θ]
{ designador_de_unit(u) }
C•Ψ |M u >> g [θ]
Para derivar a fórmula de extensão u>>g, em que u é um designador de unit, faz-se a
derivação de g no contexto corrente estendido por u. Esta derivação é efectuada em
modo externo.
Libertação
λ•Ψ |e g [θ]
C•Ψ |M ?g [θ]
O contexto corrente é substituído pelo contexto vazio. Esta regra é útil no caso em
que g é uma formula de extensão de contexto, pois dá a possibilidade da construção de
um contexto novo de raiz.
Salvaguarda
C•C•Ψ |M g [θ]
C•Ψ |M >g [θ]
O contexto corrente é duplicado no topo do contexto histórico prosseguindo a
derivação sem qualquer alteração adicional. Este mecanismo destina-se a memorizar
o contexto no qual se inicia a prova de g para reposição posterior.
3. Programação em Lógica Contextual
28
Reposição
Ψ |e g [θ]
C•Ψ |M <g [θ]
Remove o contexto corrente do topo do contexto histórico tornando portanto
corrente o último contexto que foi salvaguardado.
3.9. Exemplos
Neste ponto vamos ilustrar a utilização da linguagem com alguns exemplos de
aplicação, muito simples, baseados em [Monteiro 89a]. Todos eles apresentam
diversas formas de definir aproximadamente o mesmo programa usando vários dos
novos mecanismos da CxLP. Para cada um destes exemplos, são ainda exibidas uma
ou duas interrogações do sistema, acompanhadas pelas sequências de derivações que
originam.
Cada programa é constituído por um par de units, uma das quais é partilhada por
todos os programas. Trata-se da unit books que contém informação sobre escritores
de livros e que exporta esses dados através do predicado wrote/2:
unit books.
visible wrote/2.
wrote(plato,republic).
wrote(homer,iliad).
A outra unit, chamada authors, define autor como alguém que escreveu um livro.
Encontra-se programada em todos os casos com características genéricas.
Exemplo 1 - wrote/2 dependente de contexto
unit authors.
visible author/1.
author(Person) :- wrote(Person, _).
?- books >> authors >> author(Who).
λ
|i books >> authors >> author(Who)
books
|e books >> author(Who)
(Extensão)
authors.books
|e author(Who)
(Extensão)
authors.books
|i author(Who)
(Entrada)
authors.books
|i wrote(Who, _)
(Redução)
books
|e wrote(Who, _)
(Saída)
books
|i wrote(Who, _)
(Entrada)
books
|i true
(Redução)
sucesso [{Who/plato}]
?- authors >> books >> author(Who).
λ
|i authors >> books >> author(Who)
authors
|e books >> author(Who)
(Extensão)
books.authors
|e author(Who)
(Extensão)
authors
|e author(Who)
(Descida)
authors
|i author(Who)
(Entrada)
3. Programação em Lógica Contextual
authors
λ
falhanço
|i wrote(Who,_)
|i wrote(Who,_)
(Redução)
(Descida)
Exemplo 2 - Extensão de contexto usando parâmetro de unit
unit authors(Works).
visible author/1.
author(Person) :- Works >> wrote(Person, _).
?- authors(books) >> author(Who).
λ
|i authors(books) >> author(Who)
authors(books)
|e author(Who)
(Extensão)
authors(books)
|i author(Who)
(Entrada)
authors(books)
|i books >> wrote(Who, _)
(Redução)
books.authors(books)
|e wrote(Who, _)
(Extensão)
books.authors(books)
|i wrote(Who, _)
(Entrada)
books.authors(books)
|i true
(Redução)
sucesso [{Who/plato}]
Exemplo 3 - Predicado importado de parâmetro de unit
unit authors(Works).
visible author/1.
import wrote/2 from Works.
author(Person) :- wrote(Person, _).
?- authors(books) >> author(Who).
λ
|i authors(books) >> author(Who)
authors(books)
|e author(Who)
(Extensão)
authors(books)
|i author(Who)
(Entrada)
authors(books)
|i wrote(Who, _)
(Redução)
books
|e wrote(Who, _)
(Substituição)
books
|i wrote(Who, _)
(Entrada)
books
|i true
(Redução)
sucesso [{Who/plato}]
Exemplo 4 - Utilização do contexto histórico
unit authors.
visible author/1.
author(Person) :- <wrote(Person, _).
?- authors >> books >> (>author(Who)).
λ
|i authors >> books >> (>author(Who))
authors
|e books>> (>author(Who))
(Extensão)
books.authors
|e >author(Who)
(Extensão)
books.authors•books.authors |e author(Who)
(Salvaguarda)
authors•books.authors
|e author(Who)
(Descida)
authors•books.authors
|i author(Who)
(Entrada)
authors•books.authors
|i <wrote(Person, _)
(Redução)
books.authors
|e wrote(Person, _)
(Reposição)
books.authors
|i wrote(Person, _)
(Entrada)
books.authors
|i true
(Redução)
sucesso [{Who/plato}]
29
30
4
CxM - Máquina abstracta para CxLP
4.1. Introdução
A máquina abstracta que foi definida para suporte da linguagem CxLP recebeu o
nome de “Máquina Contextual”. No entanto ela será habitualmente referida pelo
acrónimo “CxM”. A CxM baseia-se na WAM2, que foi modificada com o objectivo de
suportar todos os mecanismos da linguagem CxLP. Para isso, foram adicionados
novos registos, novas estruturas de dados e novas instruções. Muitas das instruções
herdados da WAM foram igualmente alvo de adaptações, mais ou menos extensas. Na
introdução de todas estas alterações existiu um permanente cuidado em manter as
particularidades da WAM que fazem com que ela seja uma excelente implementação
do Prolog. Houve ainda a preocupação de organizar a nova máquina por forma a que a
sua maior complexidade não prejudicasse de forma significativa a velocidade de
execução de programas escritos em Prolog simples.
Como já dissemos, para além do desenvolvimento de uma implementação eficiente
de CxLP, neste trabalho considerámos o problema da definição de um sistema de
suporte flexível para a linguagem. O tratamento deste problema desceu ao nível da
máquina abstracta tendo sido introduzidos mecanismos na CxM com este fim. Eles
fazem parte da definição oficial da máquina abstracta, todavia, por razões de
organização, são descritos separadamente no capítulo 5.
2 Atendendo a que a publicação [Warren 83] é a referência principal, mesmo obrigatória, para a
WAM, a nomenclatura aí introduzida é seguida de perto, por forma a facilitar a tarefa do leitor
familiarizado com aquele trabalho. O único ponto em que divergimos um pouco, foi na não
consideração dos registos Ai por eles se identificarem com os registos Xi. Esta é uma questão
de pormenor.
4. CxM - Máquina abstracta para CxLP
31
Os mais importantes tópicos de discussão no âmbito da CxM são os seguintes:
política de estabelecimento de ligações entre nomes e definições de predicados, forma
de tratamento dos parâmetros das units, e representação e manipulação de contextos
simples e históricos. Começamos no entanto por dar algumas indicações sobre alguns
aspectos básicos da arquitectura da máquina: a representação dos termos e a
organização da memória.
4.2. Representação dos termos
O esquema da representação dos termos da CxM é, em termos gerais, idêntico ao da
WAM. Um termo é representado por uma palavra de 32 bits na qual se definem as
seguintes três componentes: o valor, que na maior parte dos casos é um endereço que
ocupa 26 bits; a tag, que é uma marca que associa um tipo ao termo, e que ocupa no
máximo 4 bits; e os bits de recolha de lixo que são 2 e se destinam a ser usados pelo
algoritmo de recuperação de memória (garbage collection) da CxM.
Os tipos de termos suportados pela CxM, incluindo os respectivos valores
associados, são os seguintes:
• Variável
O conteúdo de uma variável livre é uma referência para ela própria. O
conteúdo de uma variável instanciada é o valor que lhe está
atribuído.
• Átomo
Apontador para um descritor de átomo.
• Inteiro
Valor inteiro de 29 bits.
• Real
Valor real ocupando 29 bits.
• Estrutura
Apontador para o local da definição de um termo estruturado, o qual
consiste num functor e seus argumentos em células de memória
consecutivas.
• Lista
Apontador para o local da definição da lista, o qual contém os seus
dois argumentos (sem functor portanto). As
listas
têm
uma
representação distinta das outras estruturas, exclusivamente por
razões de eficiência.
Na CxM as tags têm um tamanho base de 4 bits, com a excepção das tags indicativas
de inteiro e real que têm apenas 3 bits para não afectar demasiado a precisão dos
valores numéricos. Da configuração das tags depende muito a eficiência da máquina.
No ponto 6.4 justifica-se a configuração usada na CxM, que é a seguinte:
Tags da CxM
Variável:
Lista:
Estrutura:
Átomo:
Inteiro:
Real:
0000............................
1001............................
1010............................
1011............................
110.............................
111.............................
4. CxM - Máquina abstracta para CxLP
32
4.3. Organização da memória
A organização da memória na CxM difere em alguns aspectos da existente na WAM.
Isto deve-se apenas a uma tentativa de aperfeiçoamento desta máquina, e não a
qualquer necessidade imperiosa relacionada com os contextos.
São 3 as principais áreas de dados da CxM: área de código, stacks e trail.
4.3.1. Área de código
Funciona de forma semelhante à área reservada para a criação de variáveis
dinâmicas das linguagens convencionais, sendo manipulada através das habituais
primitivas de reserva e libertação de memória. É nela que são armazenados os
descritores de átomo, functor, predicado e unit, assim como o código compilado dos
programas e os termos que correspondem à representação externa destes. Para
diminuir a fragmentação da memória, esta área encontra-se dividida em duas partes:
zona permanente e zona dinâmica. Na zona permanente são guardados os objectos que
não podem ser destruídos após criação, ou seja todos os descritores atrás referidos, e
ainda o código dos predicados de sistema. Na zona dinâmica são guardados o código e
os termos-fonte dos programas.
4.3.2. Stacks (heap + stack local)
Nesta zona de memória encontram-se as duas pilhas de execução da máquina: o
heap (ou stack global) e o stack local. O heap contém todos os termos estruturados
criados durante a execução dos programas. Por sua vez, o stack local contém
principalmente informação para controlo da execução dos programas e descrições de
contextos. Essa informação encontra-se organizada em quatro tipos de registo: registo
de activação, ponto de escolha, instância de unit e registo histórico.
As duas pilhas crescem em sentidos opostos, uma para a outra, de forma a que o
espaço livre é partilhado pelas duas. A situação de transbordo de uma pilha é
detectada através de um teste ao tamanho deste espaço livre efectuada em cada
chamada de predicado. Ele deverá ser sempre superior a um dado valor fixo, já que o
compilador garante que entre duas chamadas de predicado esse valor nunca é
excedido.
A zona de espaço livre entre as duas pilhas tem na CxM um aproveitamento muito
peculiar: é usada para colocar a pilha auxiliar usada no algoritmo de unificação geral.
Como será explicado mais tarde, esta opção oferece algumas vantagens em termos de
desempenho da máquina.
Segundo a definição original da WAM, cada uma das suas duas pilhas de execução
encontra-se numa zona distinta de memória, efectuando-se o seu crescimento no
sentido dos endereços crescentes. Esta disposição da memória destina-se a simplificar
a operação de ligação entre duas variáveis livres que, desta forma, envolve apenas
4. CxM - Máquina abstracta para CxLP
33
uma comparação de endereços e uma atribuição. No entanto, esta organização tem a
desvantagem de não tirar o melhor partido da utilização da memória, já que sempre
que o espaço atribuído a uma das pilhas se esgota, o espaço entretanto disponível na
outra não pode ser usado. Na CxM optámos por uma disposição diferente das pilhas
para
evitar
este
problema.
Entretanto
a
eficiência
da
máquina
não
fica
comprometida: não obstante ser necessário efectuar um teste suplementar quando da
ligação de duas variáveis livres, isso é compensado pela maior simplicidade do teste
que determina a necessidade de memorizar uma variável no trail quando ela é
instanciada.
4.3.3. Trail
Trata-se de uma simples pilha na qual, tal como na WAM, são anotados os
endereços de todas as variáveis que têm de ser tornadas livres quando ocorrer
retrocesso.
4.4. Ligações em CxLP
O principal factor que contribui para o poder da linguagem CxLP reside na grande
riqueza de formas de estabelecimento de ligações entre nomes e definições de
predicados. A principal característica de muitas dessas ligações é a de serem
determinadas pelo contexto corrente, que assim se revela como um ambiente de
ligação, segundo a nomenclatura tradicional das linguagens de programação.
Em CxLP existem quatro possibilidades diferentes de estabelecimento de ligações,
determinadas pelas circunstâncias particulares que rodeiam a utilização de cada
nome de predicado. Tendo em conta estas circunstâncias os nomes de predicado
podem ser classificados em quatro grupos:
•
Nomes definidos localmente.
•
Nomes dependentes do contexto.
•
Nomes ocorrendo em fórmulas de extensão.
•
Nomes importados.
Neste subcapítulo, cada um destes tipos de nome é alvo de uma análise individual,
sendo efectuado o estudo dos problemas envolvidos no estabelecimento de cada forma
de ligação. O objectivo é o da definição de mecanismos de ligação tão eficientes quanto
for possível. Esses mecanismos concretizam-se nas instruções de chamada da CxM às
quais será feita uma breve referência.
4.4.1. Ligação dos nomes definidos localmente
Em Prolog, as ligações de todos os nomes de predicado referidos no texto dos
programas, podem ser efectuadas em tempo de compilação, tendo portanto um
4. CxM - Máquina abstracta para CxLP
34
carácter estático. Em CxLP isso não pode ser feito em todos os casos. É no entanto
possível efectuar ligações deste tipo para os nomes de predicado que ocorrem com
maior frequência, que são aqueles para os quais existem definições locais na unit em
que são referidos.
A instrução da CxM gerada pelo compilador para efectuar a chamada de nomes
ligados estaticamente é a instrução Call. Só no caso dessa chamada surgir em último
lugar do corpo de uma cláusula é gerada a instrução Execute. Estas duas instruções na
CxM são idênticas às instruções homónimas da WAM.
4.4.2. Ligação dos nomes dependentes do contexto
Em CxLP a execução de um programa corresponde à prova de golos em relação a
contextos. Os pontos onde de forma mais directa a influência desses contextos se
manifesta na execução dos programas, são aqueles em que faz a derivação de golos
cujos functores principais correspondem a um nome dependente do contexto.
Recordemos que nestes casos a regra usada para estabelecer a ligação é a seguinte: a
definição de predicado associada a um nome dependente do contexto é a que se
encontra na instância de unit mais próxima do topo do contexto que inclui uma
definição visível para esse nome.
Esta regra sugere imediatamente uma forma de ligação completamente dinâmica,
estabelecida a cada invocação, por pesquisa no contexto corrente. Mas esta solução é
pouco eficiente, por causa do tempo dispendido nas buscas.
4.4.2.1. Cache
O tempo gasto nas pesquisas pode ser substancialmente reduzido através do uso de
uma cache em cada contexto, que sirva para registar as ligações dos nomes
dependentes do contexto à medida que vão sendo estabelecidas. A busca atrás referida
será assim efectuada apenas na primeira invocação de cada nome não sendo
necessário repeti-la em chamadas subsequentes. Entretanto este método tem como
pequena desvantagem o tempo dispendido a criar e inicializar a cache, operação esta
que se repete sempre que o contexto é estendido ou alterado por chamada de predicado
importado. No caso de um contexto ficar activo por pouco tempo, a intensidade do uso
da cache pode não ser suficientemente grande para compensar o tempo gasto na sua
criação. Apesar de tudo, em geral, os benefícios deste método superam largamente as
suas desvantagens, pelo que foi o escolhido para implementação.
É importante notar que, no método adoptado, a cache não é mantida quando o
contexto que lhe está associado deixa de ser necessário e é destruído. Por isso, quando
de um eventual regresso ao mesmo contexto, é necessário perder tempo a fazer a sua
reconstrução integral. Surge aqui naturalmente uma interrogação
possibilidade da memorização das caches mais
frequentemente
acerca
usadas
da
para
reutilização. Para isto ser feito, seria necessário resolver o problema fundamental de
4. CxM - Máquina abstracta para CxLP
35
encontrar um esquema associativo que permitisse reconhecer muito rapidamente
uma dada cache como sendo a correspondente a um dado contexto. Ora o facto de os
contextos serem identificados pela lista de nomes dos designadores de units
constituintes, torna este problema complexo, não tendo sido possível encontrar uma
solução suficientemente competitiva em termos de eficiência.
4.4.2.2. Estrutura da cache
Um aspecto muito importante das chamadas dependentes do contexto, e que tem de
ser considerado na definição da estrutura da cache, reside no facto de que após ter sido
encontrada a definição do predicado, este não dever ser executado no contexto da
invocação, mas sim no contexto que resulta deste por eliminação de todas as
instâncias de unit que se encontram acima daquela onde foi encontrada a definição
(cf. regras do derivação Saída e Descida). Isto significa que por cada nome dependente
de contexto que seja referido numa unit, é necessário reservar duas células da cache:
uma serve para memorizar uma referência para o predicado, e a outra o contexto no
qual esse predicado deve ser derivado. Entretanto, de acordo com o que será visto no
ponto 5.3.4, é ainda necessário considerar uma terceira célula, que é usada para
suporte da criação dinâmica de nomes dependentes do contexto.
A CxM tem uma instrução especializada para efectuar a chamada de predicados
dependentes do contexto, CtxDepJump, a qual têm dois argumentos: um functor que
corresponde ao nome invocado (para ser usado na pesquisa inicial) e um número
inteiro que indica a posição na cache associada a esse nome (para as chamadas
subsequentes).
4.4.3. Ligação dos nomes em fórmulas de extensão
Segundo a regra Extensão da definição operacional da linguagem CxLP, a prova de
um golo da forma U>>G resulta na prova do golo interno G, no contexto corrente
estendido pela unit U. Esta prova implica a descoberta no contexto estendido, da
definição de predicado visível com o mesmo nome que G que se encontre mais
próxima do topo do contexto. Ora o contexto corrente é desconhecido em tempo de
compilação. Torna-se portanto patente a ideia de que a ligação do nome do golo G terá
de ser fatalmente de natureza dinâmica.
No entanto existe um aspecto da utilização da linguagem que quando considerado,
permite num grande número de casos estabelecer formas de ligação mais eficientes. É
que regra geral, só existe interesse em usar um golo da forma U>>G quando o nome de
G é visível em U. Esta particularidade pode ser explorada de diversas formas
consoante os termos G e U sejam ou não variáveis.
Consideremos primeiro o caso de um golo u>>g, em que tanto g como u são termos
não variáveis. Se g for visível em u, é possível estabelecer em tempo de ligação, uma
ligação estática entre o nome g e a definição existente em u. Se pelo contrário, g não for
4. CxM - Máquina abstracta para CxLP
36
visível em u, então como só o topo do contexto corrente é conhecido em tempo de
ligação3, ela tem de ser estabelecida dinamicamente a cada invocação do golo de
extensão, por pesquisa no contexto corrente. No primeiro caso usa-se a instrução
CtxExtJump que tem como argumento um predicado, no segundo um instrução
chamada CtxExtJumpBelow que tem como argumento um functor.
Estudemos agora a fórmula U>>g, em que g é um termo não variável e U uma
variável. Este é um caso importante pois ocorre muito frequentemente em units
parametrizadas. Infelizmente o esquema de ligação, aqui também, parece ter de ser
dinâmico, visto que o valor de U só poder ser conhecido em tempo de execução, e poder
variar de forma imprevisível. No entanto como na maior parte destes casos o nome de
g será visível em U, não é de esperar que a demora no estabelecimento da ligação seja
muito grande. De facto, em todos esses casos, o predicado correspondente é logo
encontrado na unit corrente.
Apesar de tudo, como está aqui envolvida uma pesquisa dentro da unit corrente,
torna-se conveniente introduzir uma pequena optimização, que se baseia no facto que
a probabilidade de U representar a mesma unit em duas invocações sucessivas do golo
ser bastante grande (especialmente se U for um parâmetro de uma unit). A optimização
consiste na utilização de uma pequena cache na qual se memoriza o par <unit U,
definição de predicado para g> da última chamada. É assim possível numa chamada
subsequente testar se o valor da variável U não mudou, e neste caso usar a referência
do predicado previamente guardada. Esta optimização é especialmente importante no
caso em que o golo de extensão aparece num predicado recursivo em que U não varia.
A instrução da CxM que é usada para efectuar a chamada de um nome nestas
circunstâncias é a instrução VarCtxExtCall, a qual tem como argumentos o functor
correspondente ao nome invocado e o endereço de um par de palavras de memória que
são usadas como cache.
Restam para estudar os casos U>>G e u>>G em que G e U são variáveis. Aqui
optámos por gerar sempre chamadas do meta-predicado call/1, o que corresponde a
tratar a ligação de forma completamente dinâmica. Repare-se que teria sido possível
usar uma cache semelhante à do caso analisado no parágrafo anterior. Mas a
circunstância da inferior probabilidade de repetição do par <unit, predicado> parecem
não a justificar em geral. Não será no entanto de excluir a sua utilização em algumas
situações particulares (por exemplo no predicado bagof), só possíveis de detectar
através do uso de um compilador-optimizador que efectue análise global de
programas.
3Não pode ser em tempo de compilação pois estamos a assumir compilação separada, e
normalmente a chamada u>>g é feita do exterior de u.
4. CxM - Máquina abstracta para CxLP
37
4.4.4. Ligação dos nomes importados
A invocação de um golo g importado de uma unit U, conduz à prova de g no contexto
que resulta de substituir o topo do contexto corrente, pela instância de unit
correspondente ao termo U (cf. regra Substituição). Isso passa, como de costume, pela
descoberta no novo contexto, da definição de predicado visível com o mesmo nome,
que se encontra mais próxima do topo.
Apesar de ser aqui aparente, ainda mais uma vez, a necessidade de estabelecer a
ligação dinamicamente, acontece que circunstâncias idênticas às que permitiram
efectuar optimizações no caso das fórmulas de extensão, se repetem neste caso.
Concretamente verifica-se que se um predicado g é importado de uma unit U, então a
situação normal é aquela em que g está declarado como visível em U. Se isso se
verificar, e caso U não seja uma variável, é possível estabelecer uma ligação estática
para g, em tempo de ligação usando a instrução Call ou Execute. Se g não for visível
então é necessário usar a instrução ImportJumpBelow que tem características
dinâmicas.
No caso em que U é uma variável (trata-se neste caso necessariamente de um
parâmetro de unit), pode ser usado um método semelhante ao que foi utilizado no golo
U>>g, o qual, recorde-se, envolvia o uso de uma pequena cache para evitar o repetido
restabelecimento da ligação. Neste caso é gerada a instrução VarImportCall que tem os
mesmos argumentos que a instruçãoVarCtxExtCall: o functor correspondente ao
nome invocado e o endereço da cache de duas palavras.
Estes dois casos esgotam todas as possibilidades da importação de nomes.
4.5. Parâmetros das units
A quantificação implícita dos parâmetros de uma unit é diferente da quantificação
implícita das variáveis normais de uma cláusula, pois abrange toda a unit. Isto
significa que os mecanismos existentes na WAM para suporte de variáveis não são
adequados para os implementar. Se insistíssemos muito em usar apenas esses
mecanismos, teríamos de fazer o compilador adicionar a cada predicado pertencente a
uma unit parametrizada, novos argumentos invisíveis, tantos quantos os parâmetros
da unit, como forma de manutenção dos seus valores no âmbito de toda a unit. Este
método é pouco conveniente, tanto em termos de ocupação de memória, como em
termos de velocidade de execução.
O método usado na CxM passa pela introdução de variáveis de um novo tipo, que em
vez de residirem nos registos de activação dos predicados (como as variáveis Y) ou nos
registos da máquina abstracta (como as variáveis X), antes se encontram nas
instâncias de unit que compõem o contexto. A introdução destas novas variáveis, a
que chamaremos “variáveis Z”, obriga à introdução de diversas instruções novas na
4. CxM - Máquina abstracta para CxLP
38
máquina abstracta para lidar com elas. Felizmente as novas instruções são em
número inferior às instruções já existentes para tratamento das variáveis normais.
Isto deve-se ao facto de não serem admitidas estruturas ou constantes como
parâmetros formais das units, ao contrário do que se passa com os argumentos das
cláusulas onde, como se sabe, não há qualquer restrição. Tomando como referência a
WAM, o conjunto de instruções das classes Put e Unify tem de ser ampliado, não sendo
no entanto necessárias novas instruções do tipo Get.
A inicialização dos parâmetros de uma unit é feita no momento da criação das suas
instâncias, com base nos valores dos argumentos actuais que são passados em registos
X.
4.6. Implementação dos contextos
4.6.1. Representação dos contextos
Cada contexto na CxM é representado em memória por uma pilha de objectos
chamados instâncias de unit. Durante a execução dos programas os contextos vão
sendo alterados por adição ou remoção destes objectos.
A disciplina que rege o comportamento dos contextos sugere que a zona de memória
adequada ao seu armazenamento seja o stack local. Com efeito uma das principais
característica do stack local é o facto de ser utilizado para guardar entidades que têm
um certo carácter temporário, sendo esta uma das propriedades das instâncias de unit.
Para provar o carácter volátil destes objectos observe-se que, por exemplo na execução
duma fórmula de extensão u>>g, a instância de unit que é empilhada para a derivação
de g, deixa de ser necessária logo que g tenha sido resolvido deterministicamente ou
falhado. Este comportamento é em tudo idêntico ao dos registos de activação dos
predicados que também são desempilhados naquelas duas circunstâncias.
Os campos que constituem as instâncias de unit são os seguintes:
•U
Apontador para o descritor da unit associada.
•C
Apontador para a instância da unit anterior no mesmo contexto. Este
campo é usado nas buscas que são efectuadas para estabelecimento de
ligações dos nomes dependentes do contexto, e ainda para repor o contexto
no caso das fórmulas de extensão.
•S
Apontador para o topo do contexto anterior à criação desta instância de
unit. É usado apenas no caso particular em que a mudança de contexto é
provocada pela chamada de um predicado importado com o objectivo de
repor o contexto.
• Zi
Os parâmetros da unit. São inicializados a partir dos registos X onde se
encontram os parâmetros actuais da unit.
4. CxM - Máquina abstracta para CxLP
•N
39
Número de nomes dependentes do contexto existentes na unit no momento
da criação desta instância de unit. No fundo indica a dimensão da cache de
nomes dependentes do contexto.
• Ci
Células da cache. Cada nome dependente do contexto da unit tem
associado um índice nesta cache. As células da cache são inicializadas
com
um
valor
(nil)
que
convencionalmente
estabelecida. Posteriormente, sempre
que
a
indica
ligação
ligação
de
um
não
nome
dependente do contexto é estabelecida, fica aqui memorizada para
posterior reutilização. Quando a ligação não pode ser estabelecida por
inexistência de definição aplicável, é usado o endereço da instrução Fail.
A existência do campo N só se justifica se for permitido efectuar em tempo de
execução a adição de novas clausulas à unit, através do uso do predicado de sistema
assert/1 ou suas variantes. Nesse caso torna-se possível a criação dinâmica de nomes
dependentes do contexto para os quais não foi previsto espaço na cache quando ela foi
criada. Assim o campo N serve para detectar referências a índices de células da cache
que excedem os limites desta, caso em que a ligação do nome ao predicado tem de ser
feita dinamicamente a cada invocação. Entretanto, note-se que depois de terem sido
adicionados novos nomes a uma unit, as novas instâncias de unit a serem criadas
futuramente, já terão caches com o tamanho adequado.
4.6.2. Registos para suporte dos contextos
Todos os registos da WAM (P, CP, E, B, H, HB, TR, S, Xi ) mantêm-se na CxM, tendo
sido criados alguns novos para lidar com os contextos.
O registo que indica o contexto corrente chama-se “C” . Contêm em cada momento
uma referência para a instância de unit que se encontra no topo do contexto corrente.
O seu valor pode ser modificado em quatro circunstâncias diferentes das formas que
se a seguir se apresentam:
• Extensão de contexto - O contexto é simplesmente ampliado com uma nova
instância de unit, para a qual C fica a apontar.
• Invocação de predicado importado - A instância de unit que se encontra no topo do
contexto corrente é substituída logicamente por outra instância da unit, para
a qual fica o registo C fica a apontar. Esta substituição é implementada
através da adição ao contexto corrente de uma nova instância de unit, cujo
campo C fica a referir o segundo elemento a contar do topo do contexto em que
foi feita a invocação do predicado importado.
• Invocação de predicado dependente de contexto - O registo C passa a indicar mais
abaixo no contexto corrente, a instância de unit na qual foi encontrada a
definição do predicado. Desta forma o tamanho do contexto corrente é
reduzido.
4. CxM - Máquina abstracta para CxLP
40
• Retrocesso - É reposto o valor de C, que se encontra memorizado no último ponto
de escolha.
Uma mudança do contexto é sempre temporária, mantendo-se apenas durante a
derivação de um golo. Terminada a derivação é sempre necessário repor o contexto,
fazendo o registo C retomar um valor anterior. Nos casos de extensão de contexto e de
chamada
de
predicado
importado,
isso
faz-se
de
forma
simples
usando
respectivamente os campos C e S da instância de unit corrente, que é desempilhada.
O caso da invocação de um predicado dependente de contexto, é bastante mais
complicado de tratar, exigindo a introdução de um novo registo na CxM - o registo
“CC” (context continuation). Este funciona de forma algo semelhante à do registo CP
(continuation pointer) da WAM. No momento em que é feita a chamada do nome
dependente de contexto, o valor corrente de C é copiado para CC sendo o seu valor
preservado nos registos de activação de todos os predicados entretanto invocados.
Finalmente, na execução da instrução Proceed (que faz o retorno do predicado) C
recebe o valor de CC sendo reposto o contexto inicial.
Mas existe ainda outro problema que a invocação de predicados dependentes do
contexto coloca. É que apesar da chamada destes predicados implicar a redução de
tamanho do contexto corrente, todas as instâncias de unit que se encontram acima do
seu novo topo têm de ser preservadas por forma que o contexto antigo possa ser
reposto mais tarde. A solução mais intuitiva para este problema consistiria na
introdução de mais um registo na máquina chamado por exemplo CT (topo do
contexto) que conjuntamente com os registos E e B, ajudaria a determinar o limite
superior da parte a preservar no stack local. No entanto esta solução envolveria duas
percas de eficiência com alguma relevância: em primeiro lugar, a operação de
determinação do topo do stack local ficaria menos eficiente pois passaria a
corresponder à determinação do mínimo entre três valores; em segundo lugar, o uso de
três registos na gestão dos contextos aumentaria demasiado o tamanho dos pontos de
escolha, pelo que as operações de salvaguarda e reposição do estado da máquina se
tornariam mais pesadas. Foi felizmente possível encontrar uma solução que usa
apenas os registos C e CC associados aos contextos, e que mantêm a eficiência da
determinação do topo do stack local da WAM. Passa pela utilização de um novo
registo, chamado L (de local), que vai conter em cada momento o máximo entre o valor
do registo B e o valor do hipotético registo CT, que foi atrás referido. Adaptando
cuidadosamente as instruções da CxM ao seu uso, consegue-se manter plenamente a
eficiência da máquina e ainda evitar a necessidade de guardar o registo L nos pontos
de escolha. Apenas as instruções que implementam a operação cut ficam um pouco
menos eficientes, pois passa a ser necessário fazer explicitamente a recuperação de
espaço no stack local modificando o valor de L. Essa recuperação, na WAM, é feita de
forma indirecta, por alteração do valor do registo B.
4. CxM - Máquina abstracta para CxLP
41
Um aspecto curioso, que resulta das alterações sofridas pelos contextos quando da
chamada de predicados dependentes do contexto, é o facto de o conjunto de contextos
formar uma estrutura de dados bem conhecida: um cacto. O mesmo se passa com os
contextos históricos. Entretanto como essa estrutura já aparecia na WAM, na
organização dos registos de activação em virtude do não-determinismo do Prolog,
podemos observar que em CxLP surgem três tipos distintos de cactos que se
entrelaçam de forma complexa.
4.6.3. Instruções para controlo dos contextos
Existem seis instruções na CxM que permitem fazer o controlo directo do contexto
corrente; três delas são geradas durante a compilação de fórmulas de extensão e as
restantes na compilação de invocações de nomes importados.
A instrução PushContext é usada nas fórmulas de extensão u>>g, em todos os casos
em que u não é uma variável. Ela faz a extensão do contexto corrente, o que
corresponde a empilhar uma nova instância de unit, fazer a sua inicialização e pôr o
registo C a apontar para ela. Os argumentos actuais do designador de unit vêm nos
registos X da CxM. Ao contrário do que se passa com as chamadas de predicados, não é
obrigatório que primeiros registos (X0,X1,.…) sejam usados. Esta opção destina-se a
possibilitar a manutenção das optimizações efectuadas pelo compilador no uso dos
registos, aumentando a eficiência das fórmulas de extensão (cf. 7.5).
O código gerado pelo compilador para os golos de extensão termina sempre com a
instrução
PopContext.
Esta
instrução
testa
se
o
golo
foi
resolvido
deterministicamente, o que pode ser feito facilmente verificando se C é igual a L4; em
caso afirmativo, o espaço ocupado pela instância de unit que se encontra no topo é
libertado fazendo-se L receber o valor antigo de C que se encontra guardado na própria
instância da unit. Finalmente C recebe também esse valor, o que corresponde à
reposição do contexto.
Vejamos um exemplo de utilização destas duas instruções. O código que o
compilador gera para a clausula:
a(X,Y,Z) :- u(Z)>>g(X,Y)
é o seguinte (admitindo que g/2 é visível em u e que portanto foi possível estabelecer
uma ligação estática):
PushContext u 2 0
Call g/2<u>
PopContext
Proceed
Na instrução PushContext, o primeiro argumento é uma referência para a unit
usada; o segundo indica qual o índice do primeiro elemento do grupo de registos usado
4Se existisse um ponto de escolha por cima do topo do contexto corrente, então L estaria a
apontar para ele.
4. CxM - Máquina abstracta para CxLP
42
para passar os argumentos da unit; o terceiro indica o número de variáveis
permanentes da cláusula, que estão activas no ponto de invocação da fórmula de
extensão, e serve para a determinação do topo do stack local, para ser empilhada a
nova instância de unit.
Além das duas instruções referidas, existe ainda uma terceira que é usada nos casos
em que o primeiro argumento da fórmula de extensão é uma variável. Trata-se da
instrução PushVarContext que cria e inicia uma instância da unit com base num
designador de unit que é passado num registo X. Como a unit é variável, esta instrução
é sempre usada em associação com a instrução VarCtxExtCall ou com a chamada do
predicado de sistema call/1.
As três instruções de manipulação do contexto usadas na compilação de predicados
importados, têm um comportamento análogo ao das instruções relacionadas com as
fórmulas de extensão. Os nomes delas são SwitchContextOn, SwitchVarContextOn e
SwitchContextOff. As duas primeiras efectuam a substituição da instância unit que se
encontra no topo do contexto, aplicando-se a primeira ao caso em que a importação é
feita de um designador de unit e a segunda ao caso em que a importação é feita de um
parâmetro de unit. A instrução SwitchContextOff, é executada depois do predicado
importado retornar, repondo o contexto que existia anteriormente.
4.6.4. Fórmulas de libertação de contexto e a unit []
A unit [] (nil) é uma unit pré-definida que intervém de uma forma muito especial na
definição dos contextos simples, encontrando-se na base de todos eles. É nela que
terminam todas as buscas de definições de predicados dependentes do contexto que
não tenham sucesso.
A introdução desta unit destina-se fundamentalmente a apoiar o mecanismo de
implementação das fórmulas de libertação de contexto. Com efeito a fórmula ?G pode
definir-se por recurso à unit [] da seguinte forma:
?G :- []>>G.
Desde que a unit [] não contenha qualquer definição de predicado, a sua presença na
base de todos os contextos pode ser ignorada de um ponto de vista lógico. Por exemplo,
o contexto lógico [books,
authors(books)] é internamente é representado pela
sequência de units [books, authors(books), []], mas este facto pode ser geralmente
ignorado pelo utilizador do sistema se a unit [] estiver vazia. Parece assim ser
conveniente a proibição da adição de definições de predicados na unit [].
No entanto houve um motivo forte, de natureza prática, que nos conduziu à não
imposição de qualquer restrição à utilização da unit []. Trata-se da possibilidade da
sua utilização como zona de dados globais do sistema, que resulta de ela pertencer
simultaneamente a todos os contextos (mesmo ao contexto logicamente vazio). Entre
outras vantagens, o sistema torna-se mais facilmente usável para desenvolver e
4. CxM - Máquina abstracta para CxLP
43
correr programas escritos em Prolog (estes correm no contexto logicamente vazio ao
qual corresponde a unit corrente []), e simplifica-se também o processo de tradução
progressiva de programas Prolog para CxLP. É no entanto importante notar que
quando a unit [] é usada desta forma, a sua presença nos contextos já não pode ser
ignorada do ponto de vista lógico.
4.7. Implementação do contexto histórico
4.7.1. Representação do contexto histórico e registos associados
Um contexto histórico é representado por uma pilha de objectos denominados
registos históricos. A única função um registo histórico é a de memorizar o contexto
corrente para posterior reposição. Na memorização de um contexto basta considerar
simplesmente a referência para o seu topo.
A experiência já obtida no estudo dos contextos simples conduz-nos rapidamente a
um primeiro esquema (tentativo) de representação e gestão dos contextos históricos.
Um registo histórico deverá assim ser constituído por dois campos: um apontador
destinado à memorização do topo de um contexto simples e um apontador para o
registo histórico seguinte dentro do contexto histórico. Os novos registos da máquina
serão dois: C2 que aponta para o topo do registo histórico corrente e CC2 que
memoriza o valor de C2, para posterior reposição, sempre que o contexto histórico é
ampliado (por utilização do operador >) ou reduzido (por utilização do o operador <).
Entretanto o registo L passará a conter sempre o valor mínimo entre o registo B e dois
registos hipotéticos CT e CT2.
Apesar de este método ser já bastante razoável, da sua análise resulta a conclusão
da conveniência de eliminar o registo CC2. É que, apesar da importância das
facilidades oferecidas pelo contexto histórico, elas não precisam de ser usadas com
muita frequência na prática. E a eventual utilização de um registo CC2 afectaria um
pouco a eficiência geral da CxM, mesmo nos casos em que o contexto histórico não
estivesse a ser usado. Como se poderá observar, na descrição do método modificado
que adoptámos, é realmente possível dispensar o registo CC2, desde que estejamos
dispostos a aceitar, na utilização dos mecanismos de salvaguarda e reposição de
contexto, uma pequena perda de eficiência e um dispêndio ligeiramente maior de
memória.
Uma das particularidades do método que usamos, é o facto de pilha que representa
o contexto histórico crescer, tanto quando se faz uma salvaguarda de contexto, como
quando se faz uma reposição de contexto. Isto significa que no novo modelo, é menos
directa a relação entre o contexto histórico conceptual e a estrutura de dados que o
representa. Nas salvaguardas de contexto, o crescimento do contexto histórico tem,
como no modelo anterior, o fim de memorizar o contexto corrente para reposição
4. CxM - Máquina abstracta para CxLP
44
futura. No caso de reposição de contexto, o objectivo da ampliação do contexto
histórico é duplo: em primeiro lugar, serve para memorizar o contexto corrente, para
preparar a futura anulação da reposição de contexto que, como se sabe, é válida apenas
durante a derivação de um golo (este era o papel desempenhado pelo registo CC2); em
segundo lugar, destina-se a guardar uma referência para o registo histórico a usar caso
seja feita nova reposição de contexto (esta acção representa uma simulação da redução
de tamanho do contexto histórico conceptual: a referência indicada corresponde ao
seu novo topo).
Note-se bem que nesta implementação do contexto histórico, nem sempre o registo
que se encontra no seu topo, contém o contexto a ser usado na próxima operação de
reposição. O registo do topo contêm sim um apontador, R2, que referencia o registo
histórico no qual se encontra o contexto a ser a ser usado na próxima operação de
reposição.
Cada registo histórico é assim constituído pelos seguintes três campos:
•C
Referência para o contexto memorizado.
• C2
Apontador para o registo histórico anterior dentro do contexto histórico.
• R2
Referencia para o registo histórico que contém o contexto a ser reposto em
caso de utilização do operador <. Note-se que é possível a auto-referência
caso em que os topos do contexto histórico lógico e do contexto histórico
implementacional coincidem.
4.7.2. Instruções para controlo do contexto histórico
A CxM tem quatro instruções para manutenção do contexto histórico.
O código gerado para a clausula:
c :- >g.
onde é feita uma salvaguarda de contexto, é o seguinte:
PushHistoricalContext 0
Call g/0
PopHistoricalContext
Proceed
A instrução PushHistoricalContext empilha no contexto histórico um registo novo
no qual são guardados o contexto corrente C, o contexto histórico anterior C2 e uma
auto-referência. Desta forma o golo g é derivado já com o contexto corrente
memorizado. A instrução PopHistoricalContext repõe o valor de C2 e liberta ainda o
espaço ocupado pelo topo do contexto histórico caso este tenha sido resolvido
deterministicamente.
O argumento da instrução PushHistoricalContext indica o número de variáveis
permanentes activas na cláusula, no ponto de invocação da fórmula de extensão,
4. CxM - Máquina abstracta para CxLP
45
sendo usado apenas para determinar o topo do stack local, no momento de ser
empilhado o novo registo histórico.
No caso de uma clausula contendo uma operação de reposição de contexto, como no
exemplo:
c :- <g.
o código gerado é o seguinte:
EnterHistoricalContext 0
Call g/0
ExitHistoricalContext
Proceed
A função principal da instrução EnterHistoricalContext é a de repor o contexto que
se encontra no topo do contexto histórico lógico. Mas antes disso ser feito é empilhado
um registo histórico no qual se guarda o contexto corrente C, o valor de C2 e ainda, o
campo R2 do novo topo do contexto histórico lógico (R2 recebe portanto uma
referência para o segundo elemento, contando a partir do topo, do contexto histórico
conceptual que, desta forma, é reduzido).
A instrução ExitHistoricalContext repõe o valor de C e C2, e liberta ainda o espaço
ocupado
pelo
topo
do
contexto
histórico
caso
g
tenha
sido
resolvido
deterministicamente.
4.8. As outras estruturas de dados do stack local
O stack local além de instâncias de unit e registos históricos contém outros dois
tipos de registos herdados da WAM: registos de activação (environments) e pontos de
escolha (choice points). Na CxM estas estruturas de dados mantêm todos os seus
campos tendo sido adicionados alguns novos.
4.8.1. Registos de activação
Os registos de activação na CxM são ampliados com um novo campo, que é usado
para guardar o registo CC. O novo formato é portanto a seguinte:
• CE
Serve para memorizar o valor que do registo E anterior à criação do
registo.
• CP
Salvaguarda o registo CP, pois o seu conteúdo pode ser destruído pela
execução da cláusula associada a este registo de activação.
• CC
Salvaguarda o registo CC, pelas mesmas razões.
• Yi
Variáveis permanentes da clausula associada a este registo de activação.
4.8.2. Pontos de escolha
A CxM tem mais quatro registos que a WAM: C, CC, C2 e L. Destes registos o único
que não precisa de ser salvaguardado nos pontos de escolha é o registo L. Daqui resulta
que o formato dos pontos de escolha da CxM é o seguinte:
4. CxM - Máquina abstracta para CxLP
46
• E, CP, B, TR, H, C, CC, C2
Servem para memorizar os valores dos registos com o mesmo nome.
•P
Endereço do código da próxima cláusula a tentar.
• Ai
Servem para salvaguardar os valores dos argumentos actuais da chamada
do predicado não determinista que provocou a criação deste ponto de
escolha.
4.9. A C-WAM
Existe pelo menos um exemplo de definição de uma máquina abstracta para uma
variante da linguagem CxLP, que é anterior a este trabalho. Trata-se da máquina
C-WAM que se encontra
descrita
na
referência
[Lamma 89].
A
linguagem
implementada suporta a partição de programas em units e a utilização de contextos
como em CxLP. Existem ainda outros mecanismos, de entre os quais se destaca um que
é designado como lazy binding e que suporta uma metodologia de programação
orientada
por
objectos. Não fazem
no
entanto
parte
da
linguagem
units
parametrizadas, nomes de predicado importados e contextos históricos.
Algumas das ideias
da
CxM
foram
directamente
inspiradas
na
C-WAM,
nomeadamente a forma de representação os contextos simples e o mecanismo de
chamada de predicados dependentes do contexto. No entanto, ao nível da realização,
elas foram por nós melhor integradas na filosofia geral da WAM, tendo resultando
numa implementação mais elegante e eficiente.
47
5
Mecanismos de ligação
incremental da CxM
5.1. Introdução
Desde o início deste projecto, que atribuímos muita importância, ao problema da
integração da implementação da nossa linguagem, como componente básica, do seu
ambiente de programação. Esta ideia fundamenta-se no princípio conhecido de que
grande parte das exigências de flexibilidade e velocidade de resposta de um ambiente
de desenvolvimento de programas, terem geralmente de ser tratadas ao nível da
própria implementação da linguagem. Esta preocupação teve um peso considerável no
desenho de todo o sistema de suporte da linguagem, com particular destaque para a
máquina abstracta.
A nossa principal preocupação em termos de ambiente de programação foi a de
evitar, desde logo, a tradicional visão dos programas como produtos acabados que
precisam apenas de ser compilados antes de serem executados. Pretendemos, pelo
contrário, aderir à perspectiva oposta segundo a qual os programas são entidades em
vias de desenvolvimento, portanto geralmente num estado incompleto e mutável. Esta
visão é importante quando consideramos, por exemplo, os processos de depuração e
teste dos programas. Mas ganha especial relevância quando lidamos com os
programas sofisticados e de grande porte, que são correntes na área da Inteligência
Artificial. Nesta situação não é geralmente possível fazer a especificação prévia
completa dos programas, pelo que o seu desenvolvimento acaba por constituir um
verdadeiro processo experimental.
5. Mecanismos de ligação incremental da CxM
48
Torna-se assim fundamental a possibilidade de criar e executar fragmentos de
programa de forma simples e expedita, reduzindo ao mínimo a duração do ciclo
edição-compilação-ligação-teste. Do ponto de vista da escolha da técnica usada para
implementar a linguagem de programação é um facto reconhecido que o uso de um
interpretador favorece esta filosofia, sendo este método adoptado por exemplo nos
ambientes das linguagens Interlisp [Teitelman 81], Cedar [Teitelman 84] e também
nos sistemas Prolog tradicionais. Mas esta técnica penaliza consideravelmente a
velocidade de execução dos programas, pelo que muitas vezes é preciso recorrer a um
compilador para produzir a versão final executável dos programas. Entretanto
existem alguns exemplos de compatibilização da flexibilidade de utilização de uma
linguagem com o uso de compilação: Smalltalk [Goldberg 83], e na área de
programação em lógica, o Prolog da universidade de Syracuse [Bowen 86].
As características modulares de uma linguagem aumentam também as exigências
colocadas ao seu ambiente de programação. A análise deste caso interessa-nos visto
na linguagem CxLP, a noção de módulo (unit) desempenhar um papel basilar.
Classicamente,
a
modularidade
das
linguagens está
associada
aos
ubíquos
mecanismos de compilação separada e de ligação (na área da programação em lógica
um dos mais conhecidos sistemas deste tipo chama-se MProlog [Bendl 80]). Esta fase
de ligação dos módulos, além de contribuir para a impaciência do programador,
muitas vezes impõe-lhe restrições drásticas, pois muitos sistemas obrigam a que
todos os predicados invocados no programa estejam definidos na altura da ligação,
mesmo que a chamada desses predicados nunca se chegue a concretizar! Este é um
problema que também exige
atenção
e
que
não
ocorre,
por
exemplo,
nos
interpretadores visto aí as ligações serem feitas dinamicamente a cada invocação de
predicado.
5.2. O sistema de suporte de CxLP
Tendo presente as considerações do ponto anterior, foi sendo desenvolvido em
simultâneo e de forma integrada com a implementação da linguagem, um sistema
para suporte de CxLP, que pretende ser o núcleo a partir do qual um ambiente de
programação com qualidade possa crescer. A característica mais assinalável deste
sistema consiste na completa fusão entre os ambientes de compilação, ligação e
execução de programas. O programa em desenvolvimento geralmente encontra-se em
memória pronto a ser executado, e todas as alterações que lhe vão sendo introduzidas
pelo programador, são assimiladas de forma rápida e automática no estado interno do
sistema. Isso foi conseguido através do estabelecimento de uma coordenação estreita
entre o compilador de CxLP, a máquina abstracta CxM e o módulo de gestão da base de
dados do sistema.
5. Mecanismos de ligação incremental da CxM
49
Do lado da máquina abstracta, foram criados mecanismos de ligação incremental,
que permitem que todas as ligações possam ser estabelecidas em tempo de execução à
medida que os predicados vão sendo invocados.
Por sua vez, a base de dados do sistema admite ser alterada em qualquer ocasião,
através da utilização de diversos predicados de sistema definidos para o efeito5 . Essas
alterações incluem: adição de clausulas (assert/1), eliminação de cláusulas (retract/1),
eliminação de um nome de predicado (abolish/2), alteração do estado de visibilidade
de um predicado (visible/2, novisible/2), declaração de importação e sua anulação
(import/3, noimport/2). Num sistema convencional qualquer destas alterações
obrigaria à recompilação integral de todas as units afectadas. No nosso sistema
qualquer uma destas mudanças apenas provoca a anulação das ligações que ficam
inválidas, que serão mais tarde restabelecidas em execução.
A compilação é feita incrementalmente, cláusula a cláusula, através do predicado
de sistema assert/1. Note-se que devido ao dinamismo do sistema, durante a
compilação de chamadas de nomes de predicados, não chega sequer a ter significado a
simples ideia do compilador determinar o tipo de ligação a ser estabelecida, já que
esta pode variar ao longo do tempo. Veja-se, por exemplo, o caso da cláusula “a:-b.”: ao
nome “b/0”, dependendo de circunstâncias que podem ir mudando com o tempo, pode
corresponder um nome local, um nome importado ou um nome dependente do
contexto. Já que a informação indicativa do tipo de ligação a estabelecer não pode
ficar no código gerado, é guardada nos descritores de nomes de predicado da base da
dados, para ser usada no momento em que a ligação for estabelecida.
A configuração deste sistema parece responder às necessidades enunciadas no
ponto anterior. É interessante observar que no fundo ele consegue conciliar toda a
flexibilidade do uso de um interpretador com o desempenho de um compilador.
Outra vantagem que ele proporciona, e que não é negligível, é a possibilidade dos
programas escritos em CxLP poderem manipular com facilidade outros programas
escritos na mesma linguagem e já carregados no sistema. Isto permite que o ambiente
de programação possa ser desenvolvido, e posteriormente crescer por adição de novas
ferramentas, usando a própria linguagem.
A flexibilidade do sistema de suporte reside decisivamente na forma como nele são
tratadas as ligações de nomes de predicados. Este é o assunto cuja análise detalhada
ocupa a parte restante deste capítulo.
5 Em geral estes predicados não são chamados directamente pelo utilizador, mas sim pelo
ambiente de programação em resposta a alguma solicitação de alto nível por parte do
utilizador, ou ainda de forma automática durante a edição do programa por exemplo.
5. Mecanismos de ligação incremental da CxM
50
5.3. Manutenção das ligações na CxM
5.3.1. Descritores de nome de predicado
De entre os vários tipos de registos existentes na base de dados do sistema de
suporte - descritores de átomo, de functor, de unit, de cláusula e de nome de predicado estes últimos são os que naturalmente se encontram no centro do processo de
estabelecimento de ligações incrementais da CxM. Para cada nome de predicado que
seja referido numa unit, existe um destes descritores.
A lista completa dos campos constituintes de um descritor de nome de predicado é a
seguinte:
• PredCode
Contém uma referência para o código, a executar quando o nome
de predicado for invocado.
• Inst
Quando usado, contém uma instrução da CxM.
• Pred
Apontador para um descritor de nome predicado. Com excepção
dos nomes que ocorrem em fórmulas de extensão, trata-se de uma
auto-referência.
• Functor
O functor que indica o nome do predicado.
• Unit
Unit à qual pertence este descritor.
• Clauses
Lista das cláusulas que constituem a definição de um nome
definido localmente. Os descritores dos outros tipos de nomes de
predicados têm zero cláusulas associadas.
• CtxDepOffset
Deslocamento na cache de um predicado dependente do contexto.
• ImportTerm
Termo que descreve a unit de onde o nome deste descritor é
importado. Se existir alguma referência a parâmetros de unit
nesse termo, eles são representados por termos da forma
$_unit_parameter(N) em que N indica a ordem do parâmetro
referido.
• ExtUnit
Num nome interveniente numa fórmula de extensão, representa a
unit que provoca essa extensão.
• IdxHConst
• IdxHStruct
• BindType
Usados para guardar apontadores para as tabelas de hash que
podem ser geradas durante a criação do índice de um predicado
associado a um nome definido localmente.
Campo de um tipo enumerado, que indica o tipo de ligação que foi
estabelecida no descritor. Os valores possíveis são: noBind,
indexBind, importBind, otherBinds.
5. Mecanismos de ligação incremental da CxM
51
• IsVisible, IsImported, IsBuiltin, IsPermanent,
• IsStatic, CCoded, KeepSource, NoIndex
Atributos booleanos do nome de predicado.
Como se pode observar os descritores de nome de predicado contêm campos em
número suficiente para possibilitarem a representação qualquer um dos quatro tipos
de nomes existentes em CxLP:
• Para um nome definido localmente, existe a lista de clausulas que define o seu
predicado associado;
• Para um nome importado, a descrição de unit que aparece na sua declaração de
importação;
• Para um nome dependente do contexto, o seu deslocamento dentro das caches
das instâncias da unit a que pertence;
• Para um nome que ocorra numa fórmulas de extensão, a referência para a unit
que determina a extensão do contexto.
O primeiro campo de cada descritor - PredCode -, que serve para guardar o endereço
do código a executar quando o respectivo nome for invocado, geralmente aponta para
o início de um segmento de código constituído por sequência de instruções. Mas como
se verá adiante, existem algumas situações em que pretendemos que ele refira apenas
uma instrução isolada cujos argumentos são atributos do próprio descritor. Nesses
casos, a instrução é colocada no segundo campo do descritor - Inst - de forma a que os
campos que se lhe seguem sejam vistos, do ponto de vista da execução da instrução,
como os seus argumentos.
Todas as invocações de nomes predicados são efectuadas através do respectivo
descritor, para que seja possível o estabelecimento e anulação dinâmica de ligações
usando a indirecção proporcionada pelo campo PredCode.
5.3.2. Estabelecimento das ligações
Num descritor de nome de predicado que não tenha ainda sido ligado, o campo
PredCode aponta sempre para uma instrução da CxM que se encontra no campo Inst, e
que é especializada no estabelecimento de ligações. Como o conjunto de acções a
efectuar para estabelecer uma ligação varia muito com o tipo de nome de predicado,
foram definidas na máquina abstracta quatro instruções de ligação distintas:
MakeIndex, MakeImportBinding, MakeCtxExtBinding e MakeCtxDepBinding.
De modo geral, o estabelecimento de uma ligação consiste na determinação do
endereço do código a executar e na memorização desse endereço no campo PredCode de
um descritor de nome de predicado. Com este método, a ligação depois de efectuada
mantém-se fixa, não havendo perda de eficiência, para além da que resulta das
chamadas de predicado serem feitas usando uma indirecção, o que praticamente não
tem significado.
5. Mecanismos de ligação incremental da CxM
52
Faz-se de seguida uma descrição breve da funcionalidade de cada uma das quatro
instruções de ligação da CxM.
5.3.2.1. MakeIndex
O caso do estabelecimento de uma ligação para um nome definido localmente
parece ser o mais simples de tratar, pois aparentemente basta memorizar em
PredCode, o endereço do código da primeira cláusula do predicado. No entanto isto só
estaria correcto se na CxM não existissem mecanismos de indexação. Como é
suportada indexação, a instrução de ligação tem neste caso a tarefa, bastante mais
complicada, de construir o índice do predicado, sendo a ligação efectuada usando o
endereço do índice construído.
5.3.2.2. MakeImportBinding
No momento do estabelecimento da ligação para nomes importados, toda a
informação necessária está presente no descritor. Consiste no functor do predicado
importado e no termo que descreve a unit de onde se importa.
No estabelecimento da ligação há diversos casos a considerar: a unit pode estar
definida, não existir, ou ainda ser um parâmetro de unit; por sua vez o nome de
predicado pode ser ou não ser visível na unit donde é importado. A instrução
MakeImportBinding chama uma componente do compilador que trata todos estes
casos e cria geralmente um pequeno segmento de código de ligação, no qual se efectua a
chamada do predicado no contexto adequado. As instruções geradas executam em
sequência as seguintes acções: mudança de contexto, chamada do predicado externo,
reposição do contexto inicial, retorno. O caso, pouco frequente, em que o nome de
predicado importado não é visível recebe um tratamento mais simples: faz-se o campo
do seu descritor
PredCode apontar para a instrução ImportJumpBelow, cujo
argumento é um functor e efectua uma busca no contexto corrente, ignorando o seu
topo que, neste caso, não precisa de ser alterado.
5.3.2.3. MakeCtxExtBinding
O tratamento do caso das fórmulas de extensão é mais simples que o dos nomes
importados, visto que a mudança de contexto necessária à invocação do predicado, ter
sido já tratada em tempo de compilação 6 . Como no momento em que a ligação é
efectuada a unit que determinou a extensão de contexto já se encontra no topo do
contexto corrente, basta verificar se o nome de predicado invocado é ou não visível
nessa unit (cf. 4.4.3). Para cada um destes dois casos a ligação estabelece-se fazendo
PredCode apontar, respectivamente, para a instrução CtxExtJump (que tem como
argumento o descritor de nome de predicado externo que é chamado e executa um salto
6 A sintaxe das fórmulas de extensão permite que isto seja feito.
5. Mecanismos de ligação incremental da CxM
53
inter-units) ou para a instrução CtxExtJumpBelow (cujo argumento é um functor e faz
uma busca no contexto corrente ignorando o seu topo).
5.3.2.4. MakeCtxDepBinding
O mecanismo usado no estabelecimento de ligações para nomes dependentes do
contexto, e que se encontra descrito no ponto 4.4.2, tem um carácter altamente
dinâmico, que o torna um caso a ser considerado à parte. Recordemos que a técnica
usada associa a cada nome dependente do contexto algumas células de uma zona de
uma instância da sua unit a que chamámos cache. No fundo essa cache não é mais do
que um ambiente de ligação sendo aí que as ligações são estabelecidas. Para que serve
então a instrução MakeCtxDepBinding? Esta instrução é usada também para efectuar
um determinado tipo de ligação, só que de natureza diversa. Ela associa um nome
dependente do contexto a um deslocamento na cache e informa a unit a que ele
pertence, de que deve reservar as células correspondentes a esse deslocamento em
todas as suas instâncias. Trata-se portanto de uma espécie ligação de nível 2. O
estabelecimento da ligação completa-se, fazendo PredCode apontar para a instrução
CtxDepJump, a qual tem como argumento o deslocamento na cache atrás referido. É
nesta instrução que se encontra codificada toda a lógica de chamada de predicados
dependentes do contexto.
5.3.3. Invalidação das ligações
Existem diversos predicados de sistema (assert, retract, visible, etc) que afectam o
estado interno dos descritores de nome de predicado. Esses predicados de sistema,
podem ser chamados directamente pelo utilizador, mas geralmente são usados pelo
sistema de suporte ou pelo ambiente de programação em resposta a uma solicitação de
mais alto nível.
Muitas das alterações do estado interno dos nomes de predicado afectam a validade
de uma ou mais ligações já estabelecidas. Um dos casos mais simples, por afectar
apenas a ligação existente para o descritor de um único nome de predicado, é o da
adição de uma cláusula a um predicado: a ligação terá de ser restabelecida por
reconstrução do seu índice. No outro extremo da graduação da complexidade,
encontra-se a operação de mudança do estado de visibilidade de um predicado, a qual
pode invalidar múltiplas ligações, tanto de nomes dependentes do contexto, como de
nomes importados ou fórmulas de extensão.
De modo geral, quando um ligação fica inválida existem duas vias possíveis para
lidar com a situação. A primeira consiste em logo após a determinação da sua não
validade fazer o seu restabelecimento imediato. A segunda não é tão directa:
corresponde a trazer de novo as ligações inválidas, para o seu estado inicial de
ligação-por-estabelecer, deixando o seu estabelecimento efectivo para a próxima
invocação do nome. Esta última alternativa mereceu preferência devido a ser habitual
5. Mecanismos de ligação incremental da CxM
54
que as alterações da base de dados sejam feitas de forma agrupada (ex: carregamento
das diversas cláusulas de um predicado). Se a primeira alternativa fosse a escolhida,
as ligações logo depois de recuperadas estariam constantemente a ficar inválidas,
perdendo-se desta forma tempo em vão.
Fazemos de seguida o levantamento completo do efeito que os predicados de
controlo da base de dados têm nas ligações. Todos os predicados referidos actuam
sobre os nomes da unit corrente.
5.3.3.1. Efeitos da alteração do conjunto de cláusulas de uma unit
assert(Clause)
retract(Clauses)
Em relação a estes dois predicados de sistema é invalidada apenas a ligação do
nome de predicado afectado pela operação. Para se desfazer a ligação, é necessário
determinar qual a instrução de ligação da CxM que deve ser usada na circunstância,
tendo em conta os atributos com que o nome de predicado fica após a alteração. Se o
nome ficar sem cláusulas, torna-se um nome dependente do contexto e nesse caso deve
ser instalada a instrução MakeCtxDepBinding no campo Inst do descritor. Caso
contrário a instrução a usar é MakeIndex.
Note-se que este esquema, apesar de ser de forma geral muito prático, pode ter
alguns inconvenientes no caso particular de um predicado muito dinâmico, que esteja
sujeito a constantes alterações. De facto, nestas circunstancias, a maioria das
invocações do predicado acabam por provocar a reconstrução refazer do seu índice. E
como esta operação, pode ser relativamente demorada no caso em que o predicado é
constituído por muitas cláusulas, os benefícios do índice, podem ser ultrapassados
pelo inconveniente da sua constante reconstrução. Para resolver este problema foi
incluído nos descritores de nome de predicado um atributo booleano - NoIndex - o qual
indica se o índice deve afinal se ou não ser construído. Esse atributo é consultado pela
instrução MakeIndex e é posicionado pelo predicado de sistema noindex(Name,Arity).
Esta solução para o caso dos predicados altamente dinâmicos, sendo aceitável, não
é ainda no entanto a melhor, pois passa para a responsabilidade do programador um
decisão que idealmente deveria ser tomada pelo sistema de forma automática. Com
efeito parece viável associar a cada descritor de nome de predicado uma medida do seu
dinamismo, calculada pelo sistema a partir do seu historial de alterações. Durante os
períodos em que esse valor excedesse um determinado limiar, o índice do predicado
não seria reconstruído.
5. Mecanismos de ligação incremental da CxM
55
5.3.3.2. Efeitos da alteração de importação
import(Pred,Arity,UnitDescription)
noimport(Pred,Arity)
As restrições impostas na linguagem CxLP aos nomes de predicado, impossibilitam
a existência nas units, de cláusulas locais associadas a nomes importados. É por esse
motivo que os predicados de sistema import/2 e noimport/2 são apenas aplicáveis a
nomes de predicados sem cláusulas. O seu uso provoca a mudança de tipo dos nomes de
predicados, de dependentes de contexto para importados, e vice-versa. Cada mudança
destas é acompanhada pela anulação de uma eventualmente existente ligação, sendo
usada a instrução de ligação adequada ao tipo com que o nome de predicado fica. As
duas possibilidades são: MakeCtxDepBinding e MakeImportBinding. Neste caso
também só é afectada uma única ligação.
5.3.3.3. Efeitos da mudança de visibilidade
visible(Pred,Arity)
novisible(Pred,Arity)
O caso da mudança de visibilidade de um nome de predicado p pertencente a uma
unit u, é o de tratamento mais complexo pois pode afectar várias ligações ao mesmo
tempo. Como qualquer ligação que não tenha natureza local pode ser atingida por esta
mudança, façamos um levantamento organizado por tipo de ligação, de todas as que
ficam inválidas.
No que diz respeito aos nomes importados, são atingidos todos
os
que
correspondem a declarações de importação do nome p a partir da unit u. Como o tipo
no nome de predicado não muda, é usada a instrução MakeImportBinding para
preparar o seu descritor
para
o
restabelecimento
da
ligação.
Esta
concretizada, terá características completamente diferentes das que
quando
existiam
anteriormente: o segmento de código de ligação é substituído pela instrução
ImportJumpBelow e vice-versa. Para permitir a detecção rápida de todos os
descritores de nome de predicado afectados, a base de dados prevê um acesso
optimizado com base nos functores. O campo ImportedTerm daqueles descritores é
usado nessa detecção.
O caso dos nomes que ocorrem em fórmulas de extensão tem muitas semelhanças
com o dos nomes importados. As ligações aqui afectadas são as que estão associadas a
referências ao nome p, que aparecem em fórmulas de extensão, nas quais a última
unit a estender o contexto foi a unit u (como por exemplo em u>>p, ou a(zz)>>B>>u>>p).
Atendendo a que o tipo das ligações a refazer aqui também não muda, é usada para o
efeito a instrução MakeCtxExtBinding a qual no momento da ligação produzirá
código de ligação diferente do que existia anteriormente: concretamente a instrução
CtxExtJump será substituída pela instrução CtxExtJumpBelow e vice-versa. A
detecção de todas as ligações deste tipo que devem ser invalidadas é feita rapidamente
5. Mecanismos de ligação incremental da CxM
56
usando o acesso privilegiado na base de dados, que foi atrás referido. A selecção dos
descritores de nome de predicado que nos interessam, de entre os que são acessíveis a
partir do functor, é feita com base no seu campo ExtUnit no qual se encontra uma
referência para a unit que determina a extensão do contexto na fórmula de extensão.
Falta só analisar a forma como os nomes dependentes de contexto são afectados
pela mudança de visibilidade de um nome de predicado. Como nos outros casos, esta
mudança não afecta o tipo de nome de predicado que continua a ser dependente de
contexto. Mas desta vez, ao contrário do que acontecia nos dois casos anteriores,
também não invalida a ligação estabelecida pela instrução MakeCtxDepBinding. Esta
recorde-se, atribuia ao nome de predicado apenas um deslocamento nas caches das
instâncias de unit, não existindo qualquer razão para proceder à sua alteração. O
efeito provocado pela mudança de visibilidade de um nome de predicado afecta sim,
algumas das ligações estabelecidas nessas caches. Como este problema é de natureza
diversa daqueles que têm vindo a ser discutidos neste ponto, e por ele merecer especial
relevo, faz-se a sua discussão separadamente no ponto 5.3.4.
5.3.3.4. Efeitos da eliminação de um nome de predicado
abolish(Name,Arity)
O efeito deste predicado de sistema é equivalente ao da chamada sucessiva dos
predicados novisible/2, noimport/2, e finalmente retract/1 para eliminação de todas
as cláusulas do predicado a que se aplica. Desta forma o efeito que abolish/2 tem nas
ligações dos nomes de predicado fica completamente definido a partir de tudo o que foi
discutido nos pontos anteriores.
5.3.4. As ligações dos nomes dependentes do contexto revisitadas
O problema das ligações dos nomes dependentes do contexto já foi objecto de
análise nos pontos 4.4.2 e 4.6. No entanto, no contexto da discussão feita neste
capítulo, o problema da manutenção da coerência dessas ligações necessita de estudo
suplementar.
As alterações do estado da base de dados que têm impacto no estabelecimento das
ligações dos nomes dependentes do contexto, são as que resultam da adição de
clausulas, usando o predicado assert/1, e da mudança do estado de visibilidade de
nomes de predicado, usando visible/2, novisible/2.
5.3.4.1. Adição de cláusulas
Tratemos primeiro o caso da adição de uma nova cláusula a uma unit. As
complicações que aqui se originam têm a ver com a possibilidade de criação dinâmica
de novos nomes dependentes do contexto: basta para isso que o corpo da nova cláusula
contenha invocações de nomes de predicados não definidos localmente nem
importados. Quando o número de predicados dependentes do contexto aumenta, seria
5. Mecanismos de ligação incremental da CxM
óptimo que as caches de todas as
instâncias
dessa
unit
pudessem
57
crescer
simultaneamente, a fim de que as ligações dos novos nomes fosse estabelecida da
forma normal. Mas isto só é possível em relação às novas instâncias de unit criadas
depois da alteração, pois as instâncias de unit já existentes estão encaixadas no stack
local e não podem crescer. Isto significa que durante a execução de um programa
podem
aparecer
instruções
CtxDepJump que tenham
como
argumento,
um
deslocamento na cache correspondente a uma posição não existente. Este problema
obriga a que a instrução efectue um teste suplementar com o fim de ser averiguada a
validade do deslocamento. Caso ele não seja válido a ligação tem de ser feita de forma
completamente dinâmica por pesquisa no contexto sem recurso à utilização da cache.
Este teste de validade do deslocamento implica o conhecimento, do tamanho com que
a cache foi criada, valor esse que é guardado no campo N da instância da unit.
5.3.4.2. Mudança do estado de visibilidade de um nome
A mudança do estado de visibilidade de um nome qualquer tem consequências algo
drásticas em relação às ligações dos nomes dependentes do contexto. Isto deve-se ao
facto de que quando uma destas ligações é estabelecida por pesquisa no contexto
corrente, para um certo nome n, ser usada informação sobre a visibilidade de n em
todas as units que se encontram acima do ponto onde a definição do predicado foi
encontrada. Assim quando se dá uma mudança no estado de visibilidade de n numa
unit qualquer, todas as ligações estabelecidas nas caches, que envolvam esse nome
ficam postas em causa.
Para ser reposto o estado coerente da máquina, a solução naïve, que consiste em
percorrer todas as caches existentes no stack local à procura de ligações inválidas
para serem refeitas, é extremamente inconveniente porque ineficiente.
O método que foi usado para resolver este problema baseia-se na existência de um
número de versão que se associa a cada functor. Sempre que o estado de visibilidade de
um predicado é alterado o número de versão do seu functor é incrementado para
assinalar a mudança de situação. A instrução CtxDepJump usa o número de versão da
seguinte forma: sempre que estabelece uma ligação para o nome n, além de memorizar
na cache a referência do predicado que ficou ligado a n e o contexto onde deve ser
lançada sua execução, guarda também o número de versão corrente de n (a cache tem
de ser portanto constituída por grupos de três células); a partir daí, sempre que a
instrução consultar a cache nas próximas invocações de n, ela compara o número de
versão guardado na cache com o guardado no functor de n: em caso de desfasamento a
ligação precisa de ser refeita, o que se processa imediatamente por pesquisa no
contexto corrente.
5. Mecanismos de ligação incremental da CxM
58
5.3.4.3. Eficiência
Nas ligações de nomes dependentes do contexto, ao contrário do que acontece para
todos os outros tipos de ligação, o preço a pagar pela flexibilidade do sistema de
suporte adquire algum significado. O teste suplementar para verificar a validade do
argumento da instrução que indica o deslocamento na cache, e as manipulações
efectuadas com os números de versão consomem ao todo cerca de 40% do tempo total
de execução da instrução CtxDepJump o que penaliza todas as chamadas de nomes
dependentes do contexto. Entretanto num sistema em que não fosse possível de forma
dinâmica fazer a adição de cláusulas nem a alteração do estado de visibilidade de
nomes de predicado, a chamada de nomes dependentes do contexto seria mais
eficiente, sendo os referidos 40% reduzidos para 15%, que correspondem ao tempo
necessário para testar o preenchimento da posição da cache respectiva. De qualquer
forma a diferença entre estes dois valores - 25% - não parece ser suficientemente
elevada para constituir fonte de preocupação, por se localizar numa única instrução.
59
6
Técnicas de implementação da CxM
6.1. Introdução
Se tomarmos a descrição da máquina abstracta CxM que se encontra no apêndice
B, e a tentarmos codificar directamente usando por exemplo a linguagem C,
verificaremos que a eficiência da implementação obtida deixa muito a desejar,
quando comparada com a da maior parte dos compiladores de Prolog existentes no
mercado. No entanto se fizermos a cuidadosa consideração de todas as oportunidades
para a introdução de optimizações que for possível descobrir, as melhorias de
desempenho
são
muito
substanciais
podendo
facilmente
exceder
os
400%!
Sublinhe-se que isto pode ser feito recorrendo apenas a optimizações de natureza
conceptual, que não resultam da utilização de linguagem máquina.
Este capítulo é dedicado à apresentação das principais técnicas que foram usadas
com o objectivo de incrementar tanto quanto possível a eficiência da nossa máquina.
6.2. Lips!
O nosso protótipo da CxM consiste numa implementação portável da máquina,
escrita na linguagem C. Foi desenvolvida em computadores Macintosh usando o
ambiente de programação Think C, e em computadores VAX usando o compilador de
C, gcc.
Não obstante o sistema se encontrar escrito numa linguagem de alto nível, ele
oferece um desempenho bastante razoável: numa VAXstation 3540 a velocidade do
6. Técnicas de implementação da CxM
60
programa de teste tradicional, o naïve-reverse, excede os 60 KLips. É interessante
comparar este valor com a velocidade de uma das melhores implementações
comerciais do Prolog, o Quintus Prolog v2.4, que na mesma máquina e com o mesmo
programa faz cerca de 95 Klips!
Para a uma justa apreciação destes valores é conveniente notar que a comparação
que eles proporcionam ser muito grosseira. De facto, como o Quintus faz análise
global de programas, esta diferença aumenta ainda mais a seu favor nos programas
complexos da vida real. Em contrapartida a nossa implementação suporta uma
linguagem mais complexa, e está preparada para lidar com predicados dinâmicos sem
penalização de velocidade. Para exemplificar este último aspecto, usando o mesmo
programa de teste num formato dinâmico, o Quintus faz apenas 2.3Klips, que é um
valor mesmo inferior ao obtido no interpretador CProlog que faz 5Klips.
Outros dois compiladores de Prolog, muito conhecidos, são o SB-Prolog e o
SICStus-Prolog; as suas velocidades para o mesmo programa são inferiores a 35 Klips.
Nos diversos pontos que constituem este capítulo, serão feitas algumas referências
ao ganho percentual de velocidade que algumas optimizações da máquina permitiram
alcançar. Esta avaliação se não for usada com cautela pode revelar-se errónea, pois os
valores obtidos, dependem fortemente do estado geral de optimização da máquina no
momento em que as são feitas7. Devido a esta relatividade, nas percentagens que
serão indicadas toma-se sempre por referência uma mesma versão da máquina
(concretamente a do dia em que este capítulo começou a ser escrito - 15 de Junho de
1990). Esses valores reflectirão portanto a situação em que a optimização que estiver a
ser referida ter sido exactamente a última a ser introduzida na máquina.
Em geral, as percentagens indicadas aplicam-se ao programa de teste naïve-reverse
correndo numa VAXstation 3540 usando o compilador gcc. Esta última configuração
foi escolhida devido à capacidade do gcc lidar com registos globais e ao facto de no VAX
ser permitido declarar 6 desses registos desses. Desta forma a torna-se possível
abordar tópicos que em alternativa teriam requerido o uso de assembler na
experimentação.
6.3. Técnica de interpretação
6.3.1. Código threaded
Na implementação do ciclo de interpretação da máquina abstracta foi usada a
técnica chamada de código threaded directo (cf. [Bell 73]). Este método tem as seguintes
duas características distintivas:
7 Por exemplo se uma determinada optimização permite ganhar 1 segundo num programa que
demora 10 segundos a correr parece correcto dizer que o ganho foi de 10%. Mas suponhamos
que suspendemos essa optimização e que nos dedicamos a melhorar outros aspectos da
máquina. Mais tarde quabndo o mesmo programa correr em 5 segundos, a reintrodução da
optimização anterior permite agora uma ganho de 20%.
6. Técnicas de implementação da CxM
61
• Cada instrução da máquina abstracta está codificada numa rotina separada.
• O formato de cada instrução da máquina abstracta consiste no endereço da
rotina que a implementa, seguido de eventuais argumentos.
Usando a linguagem C e assumindo que cada instrução se encontra programada
numa função sem argumentos, o ciclo de interpretação de uma máquina deste tipo é
fácil de escrever:
while( true ) (*PC++)() ;
em que PC (contador de programa) é uma variável da linguagem C do tipo apontador
para função.
6.3.2. Método clássico
O método que mais tradicionalmente se usa para implementar interpretadores de
máquinas abstractas define um formato de instrução próximo do das máquinas
convencionais reais. Nesse método, que chamaremos de método clássico, todas as
instruções têm uma zona inicial fixa de identificação (código de operação), sendo os
argumentos colocados a seguir. Em cada iteração do ciclo de interpretação, é
consultado o código de operação da instrução corrente, que é usado para seleccionar o
código a executar. Este esquema, quando programado em C, é codificado num ciclo
dentro do qual se encontra uma instrução alternativa switch, a qual deverá prever
tantas possibilidades quantas as instruções da máquina abstracta. Desta forma toda
a máquina abstracta fica definida dentro de uma única função.
6.3.3. Comparação
A forma de descodificação das instruções no método descrito em primeiro lugar é
bastante mais simples do que a do método clássico. Numa máquina em que a operação
de chamada de função não seja demasiado ineficiente, o primeiro método é
consequentemente o mais rápido. No entanto se o interpretador for programado em C,
linguagem que permite a utilização de registos locais, o método clássico fica com clara
vantagem. Isto é assim porque o interpretador se encontra todo dentro de uma única
função, tornando-se agora possível declarar os mais importantes registos da máquina
abstracta, como sendo registos da máquina hospedeira.
A linguagem escolhida para o desenvolvimento do protótipo portável da nossa
máquina abstracta foi a linguagem C donde, em coerência com o que foi atrás
afirmado, parece que o método clássico deveria ter sido alvo de escolha. A razão pela
qual isto não aconteceu, deveu-se ao facto de estarmos interessados em usar a corrente
implementação, escrita em C, como base para futuras implementações não portáveis
escritas parcialmente em assembler. Essas implementações, atendendo às suas
características, já terão liberdade para usar os registos da máquina hospedeira. Em
essência o que isto significa é que estamos dispostos em prescindir de uma parte
significativa da eficiência da implementação inicial da máquina, em troca da
6. Técnicas de implementação da CxM
62
perspectiva da existência de futuras versões, as quais, sem compromissos de desenho
inicial, possam atingir elevadas velocidades. Entretanto o facto de o gcc permitir a
declaração de registos globais (contrariando o standard da linguagem C), torna
possível desde já a obtenção de versões da máquina de razoável desempenho.
6.3.4. Eliminação do ciclo de interpretação
O aspecto talvez mais crítico para a velocidade da máquina abstracta reside no seu
ciclo de interpretação. Existem técnicas conhecidas, que permitem reduzir o seu peso
no controlo da máquina recorrendo ao uso de um pouco de assembler. Como este caso é
realmente especial, foi o único em que condescendemos em utilizar de um pouco de
assembler. A técnica usada assume que no código gerado pelo compilador de C, nunca
tem lugar a criação de registos de activação nas invocação de funções desprovidas de
argumentos e variáveis locais. O método consiste em eliminar o ciclo de interpretação
substituindo-o por uma macro escrita em assembler, chamada JumpNext(), que é
colocada no final de cada instrução. Essa macro limita-se a transferir o controlo para
a instrução cujo endereço é apontado correntemente pelo contador de programa
(registo P da CxM) e a incrementá-lo.
O Vax dispõe de uma instrução-máquina que desempenha exactamente essa tarefa.
Trata-se da instrução jmp @(r)+, em que r é um registo. O Mac também possui uma
instrução semelhante, mas não foi possível utilizá-la, pois o Think C não permite a
declaração de registos globais, ficando a macro definida por 4 instruções-máquina.
O ganho de velocidade no Mac foi significativo: 10%. No entanto no Vax foi enorme:
226%. Para além da circunstância de a macro JumpNext() se reduzir a uma única
instrução da máquina hospedeira, esse valor também resulta de no Vax o mecanismo
de invocação de procedimentos ser ineficiente (devido à sua grande generalidade): a
eliminação do ciclo de interpretação permite o ganho do tempo de uma chamada de
função por cada instrução da CxM executada.
6.3.5. Código expandido
Existem muitas instruções da máquina abstracta cujo código de implementação é
extremamente compacto, resumindo-se a uma, duas ou três instruções da máquina
hospedeira. Nestes casos torna-se muito atraente a ideia de substituir no código da
CxM essas instruções pela sua definição expandida em linha.
Usando a nossa forma de codificar as instruções, teríamos necessariamente de
recorrer a um nova instrução, chamada por exemplo BeginInLine, que seria colocada
no inicio de todos os segmentos de código expandido. Essa instrução faria apenas a
cópia do contador de programa da CxM, para o contador de programa da máquina
hospedeira, a qual poderia assim executar o código expandido. É claro que no final de
6. Técnicas de implementação da CxM
63
cada um destes segmentos de código expandido teriam de existir instruções da
máquina hospedeira que retornassem o controlo para a máquina abstracta.
Uma técnica que muitas vezes se usa para simplificar este tipo de inclusão de código
em linha, consiste em alterar a própria representação das instruções da máquina
abstracta, substituindo os seus endereços por instruções de salto em que estes
endereços figuram como argumentos. Desta forma, a expansão de código pode ser feita
livremente, sem ser necessário recorrer a instruções de transferência de controlo
entre a máquina real e a máquina abstracta.
Uma variação do método anterior consiste na utilização de instruções de chamada
de subrotina em substituição das instruções de salto. As vantagens gerais da técnica
mantêm-se, ficando até simplificado o problema da gestão do contador de programa
da máquina abstracta: neste caso ficaria de certa maneira implícito constituindo
permanentemente o topo da pilha de execução da máquina hospedeira. No entanto na
maior parte das máquinas (reais) este método torna-se menos eficiente que o anterior,
pois impede que a máquina abstracta tenha o seu contador de programa num registo, o
que torna por exemplo o acesso aos argumentos das instruções mais lento.
Estas técnicas de inserção de código em linha foram aqui referidas apenas por uma
questão de completude, visto não serem usadas na corrente implementação da CxM.
Elas serão no entanto objecto de exploração futura.
6.4. Tags
No ponto 4.2 já foi descrito o esquema básico utilizado na máquina CxM para
codificar o tipo dos seus termos o qual, como se viu, recorre à utilização de tags. A
frequência com que durante a execução dos programas são efectuados testes para a
determinação do tipo de termos, torna este um aspecto de grande relevância para a
eficiência da máquina. A CxM assume um tamanho de célula de memória de 32 bits,
que é portanto o valor subjacente à discussão que se segue.
Existem duas decisões que é necessário tomar para
a
determinação
das
configurações das tags dos termos: qual o número de bits que devem ser reservados nas
células de memória para a codificação das tags, e quais os padrões de bits a utilizar.
Na concretização destas escolhas, um primeiro factor muito importante a
considerar, é o da frequência com que cada tipo de termo é testado. Quando
analisamos a definição do conjunto das instruções da CxM, verificamos que o teste
que por uma larga margem é mais usado é IsVar(t), o qual como o nome indica, permite
determinar se um dado termo é uma variável ou não. A uma grande distância dele em
termos de frequência, aparecem os testes de lista e de estrutura. Entretanto os testes de
átomo, inteiro e
real,
estão
praticamente
exclusivamente na programação de certos
ausentes;
eles
predicados
de
são
usados
sistema,
os
quase
quais,
comparativamente com as instruções da máquina, não são factor muito relevante
6. Técnicas de implementação da CxM
64
para o desempenho da máquina. Tornou-se desta maneira fácil estabelecer uma
hierarquia entre os tipos dos termos que reflecte a frequência com que são testados:
• Variável
• Lista, Estrutura
• Átomo, Inteiro, Real
Um segundo aspecto que temos de considerar na definição das tags, é a da eficiência
relativa das instruções da máquina hospedeira, para que seja tirado o melhor partido
das instruções de teste e salto existentes, através da escolha das mais eficientes. Esta é
uma tarefa que idealmente deveria ser feita em cada implementação concreta da CxM,
atendendo às características particulares de cada máquina hospedeira. No entanto,
visto não haver assinalável variabilidade nos computadores convencionais em
relação ao tipo de instruções que referimos, é possível extrair algumas conclusões
gerais aplicáveis em muitos casos. É a seguinte a hierarquia (decrescente) de eficiência
de testes que conseguimos obter:
• Teste de sinal
• Teste de bit
• Comparação com constante
• And com constante
• Teste directo da tag
(t>0)
( t & 0x2 == 0 )
( t > 0x276896 )
( t & testList == 0 )
( t & tagMask == intTag )
A eficiência do quarto teste é geralmente equivalente à do terceiro; foi incluído
apenas porque em determinadas circunstâncias ele é mais informativo que o terceiro.
Um último factor a considerar para a definição das tags consiste em evitar a
reserva de demasiados bits nas palavras, por forma a que fique espaço suficiente para
os endereços que nelas são guardados e não comprometer demasiado a precisão dos
valores numéricos.
Os três factores que foram referidos são suficientes para fazer a justificação
completa do sistema básico de tags usado na CxM, e que é o seguinte:
Tags da CxM
Variável:
Lista:
Estrutura:
Átomo:
Inteiro:
Real:
0000............................
1001............................
1010............................
1011............................
110.............................
111.............................
Em relação às variáveis foi usado o método mais eficiente que estava disponível,
tendo sido reservado o bit de sinal para a sua codificação. Além disso, todos os bits da
tag foram deixados a zero, para evitar demoras na operação de extracção do conteúdo
da variável durante as desreferenciações.
IsVar(t) ≡ t > 0
No que diz respeito às tags de listas e estruturas foram usados padrões que,
esquecido o bit de sinal, têm todos os bits iguais a zero, com excepção de um. Isto
permite a utilização de testes do tipo que se encontra em quarto lugar na hierarquia da
6. Técnicas de implementação da CxM
65
eficiência, quando for possível garantir que o termo a ser testado não é uma variável.
Essa garantia pode ser dada em todas as circunstâncias importantes. Note-se que o
teste de lista podia também ser feito usando o teste número 3, através da comparação
com uma constante (tagList). No entanto este método não se generaliza ao caso da
estrutura que já exigiria duas comparações. Por isso em ambos os casos se usa o 4º
teste.
testNoVarList ≡
testNoVarStruct ≡
0x60000000
0x50000000
IsList(t) ≡
IsStruct(t) ≡
IsListNoVar(t) ≡
IsStructNoVar(t) ≡
t
t
t
t
&
&
&
&
tagMask4 = tagList
tagMask4 = tagStruct
testNoVarList == 0
testNoVarStruct == 0
A forma de representação das constantes não é importante pois raramente as suas
tags são testadas directamente. Para testar, por exemplo, se um termo é uma constante
particular, usa-se na comparação todo o conteúdo da célula de memória, incluindo a
tag. O único aspecto que merece aqui especial referência é o facto de se usarem apenas
três bits de tag na representação dos valores numéricos a fim de não ser prejudicada a
sua precisão.
IsAtom(t) ≡
IsInt(t) ≡
IsReal(t) ≡
t & tagMask4 = tagAtom
t & tagMask3 = tagInt
t & tagMask3 = tagReal
Note-se que a utilização de 3/4 bits nas tags dos termos da CxM resulta de um
compromisso entre a eficiência dos testes de tipo e o número de bits usados. A situação
ideal em termos de velocidade seria a de reservar um bit diferente para cada tipo de
termo existente. No entanto isso não seria comportável devido ao excessivo tamanho
com que a tag ficaria.
Em relação a um método naïve de codificação das tags que envolva a utilização de
instruções de teste do 5º tipo em todos os casos, o nosso esquema de representação
proporciona um ganho de velocidade superior a 15%, em todos os casos estudados
(naïve-reverse no Vax, Mac II e Mac Plus). Constata-se por isso que vale a pena, em
cada implementação concreta da CxM, estudar o conjunto de instruções da máquina
hospedeira, com o objectivo de procurar alguma particularidade da qual se possa
extrair ainda mais eficiência. Por exemplo nos VAX, existe uma instrução muito
interessante que tem uma funcionalidade que nas outras máquinas só se consegue
usando duas instruções distintas: testa o bit menos significativo de uma célula de
memória e executa condicionalmente um salto relativo dependendo do valor desse bit.
Isto significa que no Vax é ainda mais eficiente testar o bit menos significativo de uma
palavra que o bit de sinal. A utilização desta instrução permitiu um ganho de cerca de
7% em relação ao esquema básico, o que totaliza um ganho de cerca de 23%
6. Técnicas de implementação da CxM
66
(1.15 * 1.07 = 1.2305) em relação ao esquema naïve. No VAX as tags têm a seguinte
configuração:
Tags da CxM nos VAX
Variável:
Lista:
Estrutura:
Átomo:
Inteiro:
Real:
000............................0
001............................1
010............................1
011............................1
10.............................1
11.............................1
6.5. Rotina de unificação
Um dos objectivos do uso de compilação na implementação de CxLP é o de fazer o
pré-tratamento possível, das unificações que ocorrem nos programas. O compilador,
usando informação estática extraída do programa fonte, consegue assim eliminar
grande parte do tempo dispendido em unificações durante a execução dos programa.
Apesar disso, o dinamismo inerente à execução dos programas nesta linguagem obriga
a que muitas das situações tenham de ser tratadas em tempo de execução, por recurso a
uma rotina de unificação geral. O peso dessa rotina é importante no tempo total de
execução dos programas, o que pode ser confirmado recorrendo a um profiler. Por
exemplo, no predicado naïve-reverse, 10% do tempo total de execução é dispendido
nessa rotina. Este valor pode ser considerado um limiar inferior pois a maioria das
unificações efectuadas no programa têm um carácter superficial: nunca se torna
necessário mergulhar muito fundo na estrutura dos termos para que as unificações se
concretizem. Da análise da definição das instruções que se encontram em apêndice
pode-se constatar que a rotina de unificação é invocada pela maioria das instruções
do tipo get e unify, o que explica a sua importância no desempenho geral da máquina.
A definição da rotina de unificação da CxM encontra-se no apêndice B, na zona da
definição das operações auxiliares mais importantes para o funcionamento da CxM.
Como aí se poderá observar, a rotina usa uma pilha na memorização dos argumentos
das estruturas e listas que, durante a execução do algoritmo, aguardam a sua vez de
serem considerados. Ao contrário da WAM, nós aqui não usamos uma pilha separada
para unificação, mas sim o próprio stack global. Esta decisão baseia-se na
consideração do facto de o registo da CxM que indica o topo do stack local - H -, pela
sua importância, provavelmente se encontrar num registo da máquina hospedeira.
Assim podemos incrementar o seu uso, tirando partido dele durante as unificações.
Isto não introduz nenhuma complicação adicional visto que caso a unificação falhe, o
mecanismo de retrocesso repõe o valor correcto de H; caso a unificação suceda, a pilha
de unificação fica vazia, o que significa que H retoma o valor de entrada na rotina de
unificação. Note-se que no caso de a máquina hospedeira não ter registos suficientes
para usar como variáveis auxiliares do algoritmo de unificação, é sempre possível
usar alguns da CxM (como por exemplo o contador de programa P). Temos também
6. Técnicas de implementação da CxM
67
neste caso o bónus de, em caso de falhanço, serem automaticamente repostos pelo
mecanismo de retrocesso. No entanto em caso de sucesso, é necessário repor o seu
valor inicial, o que obriga a uma salvaguarda inicial.
Outro aspecto que deve ser considerado, consiste no facto de, muitos dos casos que a
rotina de unificação trata, corresponderem a unificações superficiais, que não
chegam a necessitar da pilha de unificação. A rotina deve ter portanto características
optimistas, e só fazer a preparação da estrutura de dados auxiliar da unificação no
momento em que exista a certeza da necessidade da sua utilização. Isto obriga a
duplicar cerca de metade do código da rotina, mas é compensador visto verificar-se um
ganho geral de velocidade geral de cerca de 3% (ou seja 30% na própria rotina).
6.6. Especialização das instruções
Tal como na WAM, existem na CxM diversos exemplos de instruções que são
particularizações de instruções mais gerais. As instruções especializadas têm a
vantagem de serem sempre mais eficientes que as instruções gerais em que se baseiam.
É tarefa do compilador a detecção de todas as oportunidades para a sua utilização.
Como exemplos deste tipo de instruções, temos a instrução UnifyLocalValue que é uma
especialização da instrução UnifyValue, UnifyVoidOne que é uma particularização de
UnifyVoid e as instruções sobre listas que são especializações das instruções sobre
estruturas.
Um tipo de especialização que D. Warren sugere na pág. 20 de [Warren 83] consiste
na criação de novas versões de instruções através da fixação de um ou mais valores
dos seus argumentos. Por exemplo a instrução GetVariable Y2, X1 pode ser substituída
com vantagem por uma instrução sem argumentos chamada GetVariable_2_1. O
ganho que esta optimização proporciona é triplo: ganha-se tempo evitando o
tratamento dos argumentos em tempo de execução; o código da instrução fica mais
eficiente pois as referências aos argumentos são substituídas por referências a
constantes, e finalmente, o código que resulta da compilação fica mais compacto. A
desvantagem desta técnica resulta do facto de a máquina abstracta ficar maior e um
pouco mais complicada, por ter mais instruções.
Esta ideia foi extensamente explorada na CxM. As instruções da CxM mais
compactas ou de uso mais frequente, foram especializadas através da consideração
como casos particulares de todas as referências aos 5 primeiros registos X e às 5
primeiras variáveis Y da máquina. Evidentemente que isto gerou um grande número
de novas instruções tendo elas passado de cerca de 80 para mais de 250. Um dos casos
mais extremos de multiplicação de instruções, é o da instrução GetVariable Yn , Xi que
passou a existir em nada mais do que 36 versões diferentes, como por exemplo:
GetVariable_0_0, GetVariable_4_0, GetVariable_2 Xn , GetVariable_3 Yn, ….
6. Técnicas de implementação da CxM
68
O ganho de compacidade do código gerado que esta técnica proporcionou, é bastante
grande (cerca de 30%), visto a grande maioria dos argumentos das instruções deixarem
simplesmente de aparecer. Um aspecto curioso, consequência deste proliferar de
instruções consiste em que, do ponto de vista da execução da máquina, o efeito obtido
se aproxima daquele que se obteria se o compilador considerasse a definição das
instruções como macros, gerando sempre a sua expansão.
No VAX esta optimização proporcionou um ganho de velocidade de 48%. Entretanto
o aumento de tamanho do código objecto da máquina abstracta foi de cerca de 150%,
ficando a ocupar cerca de 35K de memória. No que respeita ao código fonte da máquina
abstracta, a utilização intensiva de macros permitiu controlar o seu crescimento, que
se cifrou num aumento de cerca de 100%, ficando com 4500 linhas de código C
(excluindo os comentários)
6.7. Utilização de registos da máquina hospedeira
O papel fulcral que os registos da CxM têm na definição do estado corrente da
computação na máquina abstracta, faz com que seja extremamente intensa a sua
utilização durante a execução dos programas. Compreende-se pois o contributo para o
desempenho da máquina que pode proporcionar a utilização de registos da máquina
hospedeira como registos da CxM.
A situação ideal em relação a este aspecto, será aquela em que a máquina
hospedeira disponha de um número de registos suficientemente grande para albergar
todos os registos de controlo da CxM, e ainda alguns registos X. Esta situação ideal
começa a tornar-se cada vez mais comum, pois nas arquitecturas mais recentes, um
número de registos de 32, é já relativamente corrente.
Mas quando os registos da máquina hospedeira registos são um recurso escasso,
torna-se necessário estabelecer uma relação de prioridade entre os registos da CxM,
com o objectivo de descobrir os mais fortes candidatos à utilização dos disponíveis.
Não é possível estabelecer esta relação de forma exacta, atendendo a que depende
significativamente do tipo de programas considerados na avaliação.
De qualquer forma existem alguns registos que se destacam de forma natural. Para
começar, o primeiro candidato é o registo P que é acedido sempre que uma instrução é
executada ou se acede a um argumento. Segue-se de perto o registo S que é usado nas
unificações das cabeças das cláusulas, e que pode ainda servir, sem conflito, para
indicar o modo corrente (escrita ou leitura) da CxM (só este pequeno truque
proporcionou um ganho de 5% de velocidade no VAX). Seguidamente vêm o registo H,
usado na criação de estruturas no stack global, e o registo E, usado na criação e acesso
aos registos de activação (incluindo variáveis permanentes) e ainda na determinação
do topo do stack local. A partir daqui a selecção dos próximos registos é menos clara.
Poderíamos argumentar que o próximo a considerar seria o registo B, pela sua
6. Técnicas de implementação da CxM
69
utilização na criação e preenchimento dos pontos de escolha, que têm numerosos
campos. Mas o facto é que estas operações são menos frequentes que as anteriores,
sendo ainda perfeitamente possível a utilização nelas de um registo auxiliar de
utilização geral (inicializado com o valor de B).
Por sua vez, a colocação de alguns registos X em registos da máquina hospedeira,
apesar de benéfica em termos de desempenho da máquina, tem o inconveniente de
obrigar à especialização de grande número instruções, muitas das quais seria
preferível manter na sua forma geral, devido ao seu grande tamanho ou ao baixo
ganho de velocidade proporcionado.
O compilador gcc no VAX permite a declaração de 6 registos globais. Os registos da
CxM que receberam a honra desta promoção foram os registos P, S, H, E e B.
Definimos ainda como registo uma variável auxiliar, da qual é feito um uso tão
intenso quanto possível. O ganho de velocidade conseguido foi de 148%8.
Note-se que a reserva de todos os registos da máquina hospedeira disponíveis
impede o compilador de C cumprir as declarações register do programa fonte da CxM, e
ainda de fazer optimizações por sua conta, usando registos. No entanto verifica-se na
prática que, se prescindirmos de qualquer um daqueles registos, deixando ao
compilador o critério da sua utilização, o desempenho da máquina baixa de forma
muito significativa.
6.8. Indexação
A indexação é uma técnica usada na WAM, e também na CxM, que permite reduzir
drasticamente o tempo consumido em retrocesso superficial dos programas. Os
métodos usados para fazer indexação na CxM são discutidos no capítulo dedicado à
compilação de CxLP, no ponto 7.6. Esta breve nota pretende apenas referir o enorme
ganho de velocidade que esses métodos proporcionam, e que no naïve-reverse é de
260% (17Klips contra 62 Klips)!
6.9. Penalizações provocadas pelos novos mecanismos
Para avaliar a incidência dos mecanismos relacionados com os contextos na
velocidade de execução de programas CxLP, utilizámos diversas versões do programa
naïve-reverse, as quais diferem entre si apenas na forma de acesso ao predicado
append/3. A versão mais rápida foi, como seria de esperar, a correspondente ao uso de
uma definição local para append/3. Por sua vez a versão que se revelou mais lenta, foi
a correspondente à importação deste predicado de um parâmetro de unit.
8Na versão da CxM, sem registos, usada nesta comparação, P permaneceu num registo para
não ser prejudicada a técnica de interpretação usada.
6. Técnicas de implementação da CxM
70
Durante experimentação foram usadas sempre listas de comprimento 30, sendo
portanto este o número exacto de chamadas (não recursivas) efectuadas de append/3.
As penalizações de velocidade obtidas para os diversos mecanismos testados, no Vax e
num Mac, tomando por referência o mais rápido (chamada de predicado local) foram
as seguintes:
•
•
•
•
•
Predicado importado de designador de unit atómico
Predicado importado de parâmetro de unit
Extensão de contexto usando designador de unit atómico
Extensão de contexto usando parâmetro de unit
Predicado dependente do contexto
Mac
7%
16%
5%
13%
3%
Vax
15%
33%
11%
28%
7%
O aspecto que mais rapidamente chama a atenção, é a grande disparidade dos
valores obtidos nas duas máquinas, a qual se justifica pela grande diferença de grau de
optimização que os compiladores usados suportam. O razoável valor obtido para o
caso dos predicados dependentes do contexto, deve-se ao facto de não envolverem a
criação de novas instâncias de unit, operação relativamente dispendiosa. Estes
valores são apenas indicativos, podendo na prática ser facilmente excedidos, tudo
dependendo das características dos programas usados.
Outra questão importante, é a de se saber em que medida é afectada a execução de
programas escritos em Prolog, em relação à WAM. Na nossa máquina não foi possível
evitar uma certa perda de eficiência nas operações que envolvem salvaguarda e
reposição de registos internos da máquina, por estes serem em maior número do que
na WAM. São as seguintes as instruções afectadas: Allocate, as variantes de
Deallocate, Proceed, TryMeElse, RetryMeElse, TrustMe, Try, Retry, e Trust. Em
algumas destas instruções são efectuadas três afectações suplementares, o que pode
prejudicar o seu desempenho em até 25% (15% em média).
De qualquer forma o prejuízo fica muito diluído pelas restantes instruções,
situando -se geralmente, na prática, bastante abaixo dos 2%. Por exemplo no caso do
naïve-reverse, que usa as instruções Allocate e Deallocate, a perda de velocidade chega
a não ser mensurável nos Vax, por ser inferior à margem de erro do tempo de CPU
fornecido pelo sistema.
Todas estas medições foram também aplicadas à implementação de CxLP efectuada
por Luís Monteiro e Vasco Pedro, que utiliza a técnica de pré-processamento com
tradução para Prolog [Monteiro 89b]. Os valores obtidos foram muito razoáveis
atendendo à técnica usada e ao facto de a eficiência não ter sido uma das preocupações
mais importantes da implementação: 50% de penalização na execução de programas
Prolog, mais cerca de 50% por cada um dos mecanismos expecíficos de CxLP.
71
7
Compilação
7.1. Introdução
A definição de máquinas abstractas constitui uma técnica de implementação de
linguagens de programação de ampla divulgação, e que tem uma especial importância
naquelas que, tal como o Prolog, têm um modelo de computação muito diferente do
que está subjacente aos computadores convencionais. Uma das particularidades mais
importantes desta técnica, reside no facto do conjunto de instruções abstractas ser de
alto nível, estando mais directamente adaptado ao programa fonte, que à máquina
real que vai executar o código objecto. Na perspectiva do compilador este facto é
positivo, pois geralmente significa que o processo de geração de código fica bastante
simples, por vezes mesmo trivial.
Na CxM, tal como na WAM, o conjunto de instruções é de bastante alto nível,
existindo mesmo uma relação relativamente directa entre um símbolo do programa
fonte e uma instrução da máquina. No entanto há um factor que complica o processo
de compilação. Trata-se do conjunto de facilidades que essas máquinas oferecem,
para a introdução de optimizações no código gerado. Estas facilidades não podem ser
ignoradas visto ser nelas que reside a explicação da sua maior velocidade e melhor
utilização da memória, relativamente a outras máquinas abstractas de concepção
mais naïve. Estamos concretamente a referir-nos ao suporte para a noção de registomáquina (os registos X), ao suporte de LCO (last call optimization), e ainda à existência
de instruções com diferentes graus de especialização, cujas condições de utilização
nem sempre são muito fáceis de detectar pelo compilador.
7. Compilação
72
Para o compilador tirar o melhor partido das potencialidades da CxM, ele deve
esforçar-se, portanto, por detectar as circunstâncias em que é possível gerar padrões
de código optimizado. Este problema na sua forma mais ambiciosa, envolve técnicas
sofisticadas de análise global de programas, que tornam o compilador um programa
complexo e, naturalmente, lento. Mas, se restringirmos o problema à compilação de
cláusulas individuais, abstraindo da sua interacção com o resto do programa, o
problema simplifica-se consideravelmente. Continua ainda, no entanto, a ser
possível introduzir importantes optimizações.
7.2. Características gerais do compilador
No nosso sistema, como tem sido dito, considerámos de primordial importância a
flexibilidade de utilização e a velocidade de resposta do sistema de suporte da
linguagem CxLP.
Para concretizar a flexibilidade de utilização foram usados mecanismos de
compilação e ligação incremental, os quais implicam um estilo de compilação
cláusula a cláusula, e impem qualquer tipo de análise global dos programas. Isto não
obsta, no entanto, a que seja incluído no sistema um compilador mais sofisticado,
para processar os programas depois de terem atingido a sua forma final. Um
programa deste tipo, adaptado à linguagem CxLP, não faz parte do âmbito deste
trabalho, mas será eventualmente objecto de trabalho futuro.
Quanto ao aspecto da velocidade de resposta do sistema, o compilador tenta
encontrar o melhor compromisso entre velocidade de compilação e qualidade de
código gerado. Para manter o desempenho do compilador a um nível elevado, ele foi
completamente escrito em C e não na própria linguagem a que serve de suporte, ao
contrário do que é habitual.
7.3. Funcionamento geral e código gerado
São três as principais estruturas de dados usadas pelo compilador: tabela de
variáveis contendo informação sobre todas as variáveis da cláusula a ser compilada,
tabela de registos que indica para cada um dos 256 registos X da CxM o seu estado de
ocupado/livre, e tabela de golos onde para cada golo se indica o tamanho corrente do
registo de activação e o número de variáveis unsafe que nele ocorrem pela última vez.
O código do compilador é constituído por um grande número de funções, muitas delas
recursivas, sendo o seu fluxo geral, durante o tratamento da cabeça ou de um golo da
cláusula, determinado pela estrutura do termo correspondente. O compilador executa
sempre, para cada cláusula, a seguinte sequência de tarefas:
• Faz uma passagem inicial pela cláusula para recolha de informação sobre as
suas variáveis. Essa informação, para cada variável, consiste no seguinte:
endereço, número de ocorrências, número de ordem do primeiro e último golo
7. Compilação
73
em que ocorre, número de ordem do argumento em que ocorre pela última vez no
seu último golo, e indicação se a sua primeira ocorrência na cláusula foi dentro
de uma estrutura.
• Com base na tabela de variáveis, determina o tipo das variáveis (temporárias,
permanentes ou parâmetros de unit), efectuando logo de seguida em relação às
variáveis permanentes, a sua localização nos registos de activação. Esta
operação envolve uma ordenação das variáveis, por forma que o trimming dos
registos de activação possa funcionar correctamente. Todos estes dados são
adicionadas à tabela de variáveis e à tabela de golos.
• Faz a atribuição de registos X às variáveis temporárias que ocorrem na cabeça
da cláusula ou no seu primeiro golo. O algoritmo de atribuição de registos é
descrito mais abaixo no ponto 7.4.
• Faz uma passagem pela cabeça da cláusula, gerando código de acordo com as
regras que se encontram definidas no apêndice B. Durante esta fase a tabela de
variáveis desempenha um papel fundamental no tratamento de todas as
referências a variáveis. Um caso que exige algum cuidados é o das estruturas que
ocorrem na cabeça das cláusulas: é necessário recorrer a uma pilha de termos
auxiliar e fazer uma gestão cuidada dos registos X, os quais, em tempo de
execução, irão ser usados como variáveis auxiliares.
• Gera código para o primeiro golo.
• No início do tratamento de cada golo da cláusula a partir do segundo, faz uma
atribuição de registos às variáveis temporárias que nele ocorrem. A geração de
código é feita segundo as regras do apêndice B e todas as referências a variáveis
passam pela consulta e eventual actualização da tabela de variáveis.
Alguns aspectos de mais detalhe do funcionamento do compilador e do código por
ele gerado merecem aqui referência:
• A
CxM
tem
instruções
especializadas
para
manipulação
de
variáveis
instanciadas com valores globais. O compilador detecta todas as oportunidades
de utilização dessas instruções para aumentar a eficiência do código gerado. Do
estudo das instruções que se encontram no apêndice B, conclui-se que existe a
garantia de uma variável ter um valor global, depois da execução das instruções:
PutXVariable Xi, Xj,
UnifyVoid N,
UnifyValue Xn ,
BuildValue Xn ,
UnifyVariable ψn , e BuildVariable ψn , com ψ ∈ {X,Y,Z}. Todos os descritores de
variáveis que se encontram na tabela de símbolos têm um indicador que é
posicionado sempre que o compilador gera uma destas instruções.
• O compilador tira sempre partido, do facto de o valor de uma variável
permanente ou de um parâmetro de unit se encontrar temporariamente num
registo, para gerar código mais eficiente.
7. Compilação
74
• Os golos que impliquem mudança de contexto necessitam de um tratamento
especial que é abaixo descrito no ponto 7.5.
• Ao contrário do que é indicado em [Warren 83], o nosso compilador só gera a
instrução PutUnsafeVariable Y n no último golo das cláusulas. Isto resulta da
decisão de adiar de um golo o trimming das variáveis permanentes. Em
alternativa àquela instrução, usamos a instrução PutVariable Yn que é mais
rápida e evita a transferência para o stack global de uma variável local, onde
teria um tempo de vida superior ao necessário.
• O problema da detecção de transbordo das pilhas de execução de CxLP é tratado
parcialmente em tempo de execução e parcialmente em tempo de compilação.
Em tempo de execução, a cada chamada de predicado é feito um teste ao espaço
livre na área dos stacks; caso seja inferior a 10 Kb considera-se ocorrência
transbordo. Entretanto se o compilador detectar que numa cláusula, entre duas
chamadas consecutivas, os stacks crescem mais do que 10 Kb, ele sugere ao
utilizador a rescrita da cláusula. Este deverá seguir o conselho se quiser garantir
o correcto tratamento de uma eventual situação de transbordo provocada pela
cláusula. Note-se que esta mensagem é de ocorrência muito pouco frequente, pois
a cláusula terá de conter um golo muito grande para isto acontecer. Este
esquema, além de ser simples, tem a vantagem adicional de ser muito eficiente,
pois localiza os testes de transbordo nas instruções de chamada de predicados.
• A instrução Allocate é sempre gerada, o mais longe possível do início no código
das cláusulas em que surge, por forma a ser evitada a criação desnecessária de
alguns registos de activação, em tempo de execução. Em geral, é colocada
imediatamente antes da primeira referência a uma variável permanente no
primeiro golo da cláusula ou, se esse golo não referir variáveis permanentes, é
colocada antes da instrução Call desse golo. O caso em que o primeiro golo é uma
das fórmulas especiais de CxLP, correspondentes ao uso dos operadores “>>”,
“>”, “<” ou “?”, impõe uma restrição adicional: a instrução Allocate deve de
aparecer algures antes da primeira instrução da CxM, que faça manipulação de
contextos.
• Um cut que ocorra como primeiro golo da cláusula, é traduzido na instrução
NeckCut. Um cut que ocorra mais no interior do corpo da cláusula, é traduzido
na instrução Cut Yn , em que Y n é uma variável permanente. Esta variável é
inicializada com o ponto de escolha corrente, no princípio do código do corpo da
cláusula, pela instrução GetLevel Y. Esta forma de implementar o cut
encontra-se descrita na referência [Warren 88].
• As disjunções são compiladas para um conjunto de predicados anónimos. As
operações cut nas disjunções implicam a adição de um argumento suplementar a
alguns deles.
7. Compilação
75
O compilador, incluindo o módulo que faz indexação de predicados, tem cerca de
2500 linhas de código, excluindo os comentários.
7.4. A tabela de variáveis
O compilador à medida que gera código, vai simulando alguns aspectos da execução
deste, com o objectivo de obter dados que fundamentem a introdução de optimizações.
Esses dados estão todos contidos na tabela de variáveis
Cada descritor de variável é constituído pelos seguintes campos:
• Var
Contém o endereço da variável a que corresponde este descritor.
O caso em que a variável é um parâmetro de unit é uma
excepção. Neste caso este campo contém uma constante inteira
com tag, representando o número de ordem do parâmetro na
unit. O campo Var constitui a chave de acesso aos descritores da
tabela.
• NOccurrences
Número de ocorrências da variável na cláusula.
• FirstGoal
Número de ordem do primeiro golo em que ocorre a variável.
• LastGoal
Número de ordem do último golo em que ocorre a variável.
• LastGoalArg
Número de ordem do último argumento em que ocorre a
variável, dentro do último golo.
• Type
Tipo da variável. O seu valor pertence ao tipo enumerado { temp,
perm, param }.
• IsUnsafe
Valor booleano usado só no caso de a variável ser permanente,
para indicar se ela é unsafe ou não.
• HasGlobalValue Durante o processo de compilação de uma cláusula, este campo
indica se correntemente existe a garantia de instanciação da
variável com um valor global (ou seja, uma constante, um
termo ou uma variável do stack global). Valor inicial: falso.
• WasReferenced
Indica se a variável já foi referenciada alguma vez durante a
geração de código. O código gerado na compilação da primeira
referência a uma variável deve, quando executado, fazer a sua
inicialização como variável livre. Valor inicial: falso, excepto
no caso dos parâmetros de unit que, por natureza, são
inicializados com valores provenientes do exterior da unit.
• FirstStruct
Indica se a variável tem a sua primeira ocorrência dentro de
uma estrutura. Importante para determinar se a variável deve
ser temporária ou permanente.
7. Compilação
• ParamIndex
76
Caso a variável seja um parâmetro de unit, este campo contém o
seu número de ordem.
• PermIndex
Se a variável for permanente, este campo contém a sua posição
dentro dos registos de activação associados à cláusula a que
pertence.
• TempIndex
Um variável independentemente do seu tipo pode, durante a
execução do programa, encontrar-se temporariamente num
registo X da CxM. O compilador à medida que vai gerando
código, vai tomando nota neste campo, do registo X onde ela se
encontra. O valor -1 indica que a variável correntemente não se
encontra em nenhum registo. Valor inicial: -1.
A maior parte da informação incluída na tabela de variáveis é recolhida durante a
fase de análise prévia da cláusula, sendo usada logo a seguir na determinação do valor
de alguns campos que ficam por preencher: Type, IsUnsafe, ParamIndex, PermIndex, e
WasReferenced.
Durante o processo de geração de código a maioria dos campos da tabela de
variáveis
não
sofre
qualquer
alteração.
As
excepções
são
as
seguintes:
HasGlobalValue, WasReferenced e TempIndex.
7.5. Algoritmo de atribuição de registos
O código gerado para um programa CxLP pode ser substancialmente melhorado,
através de uma cuidada atribuição de registos a variáveis temporárias, que reduza ao
mínimo
o número de instruções a executar. O facto de na CxM a passagem de
parâmetros ser efectuada através de registos, está na base da estratégia de atribuição
de registos que usamos, sendo usada de duas formas distintas.
A primeira forma resulta de serem vários os casos em que se pode associar uma
variável temporária a um registo que esteja envolvido na passagem de um parâmetro,
por forma a ser poupada a transferência correspondente à inicialização da variável. É
o que se passa por exemplo na cláusula seguinte:
append([],L,L).
Na forma mais naïve de tratar este caso, dá-se importância ao facto de dentro da
cláusula os registos X0, X1 e X2 já estarem ocupados pelos argumentos actuais da
chamada: desta forma atribui-se o primeiro registo livre - X3- à variável temporária
L. O código correspondente a esta atribuição de registos é o seguinte:
append/3: GetNil X0
GetVariable X3,X1
GetValue X3,X2
Proceed
7. Compilação
77
Mas uma análise mais cuidada, permite concluir que é vantajoso associar o registo
X1, onde vem o segundo argumento de append/3, à variável L, já que ela é inicializada
com o valor desse argumento. O código gerado para a nova atribuição de registos fica
mais compacto:
append/3: GetNil X0
GetValue X1,X2
Proceed
A outra forma de tirar partido da forma de passagem de parâmetros, consiste em
fazer uma análise prévia do próximo golo a ser compilado, e associar, desde logo,
determinadas variáveis temporárias a registos que intervenham na passagem de
parâmetros, e que já estejam, de qualquer forma, comprometidos com a passagem dos
valores das variáveis. Por exemplo a atribuição óptima de registos para a cláusula:
append([H|L0],L1,[H|L2]) :- append(L0,L1,L2).
é {L0/X0, L1/X1, L2/X2}, à qual corresponde o código:
append/3: GetList X0
UnifyVariable X3
UnifyVariable X0
GetList X2
UnifyVariable X3
UnifyVariable X2
Execute append/3
Neste caso o código da cabeça da cláusula está organizado de tal forma que da sua
execução resulta o carregamento automático dos registos para a chamada do golo que
se encontra no corpo.
Infelizmente, ao contrário do que se passa nestes dois casos, nem sempre é possível
encontrar uma atribuição de registos que permita a introdução de
optimizações, já
que podem surgir conflitos irresolúveis entre os dois critérios que considerámos. Por
exemplo na cláusula:
p(X,Y) :- q(Y,X).
a sua cabeça recomenda a atribuição de registos {X/X0, Y/X1}, mas o seu corpo
recomenda {Y/X0, X/X1}. Nesta situação extrema, qualquer que seja a atribuição
escolhida é necessário recorrer ao uso de um registo auxiliar e a diversas operações de
transferência entre registos. Eis o melhor código que é possível conseguir neste caso:
p/2:
PutValue X0,X2
PutValue X1,X0
PutValue X2,X1
Execute q/1
Ocorrem na prática muitas situações de natureza menos simples do que as três que
analisámos, para as quais a determinação de uma boa atribuição de registos, que leve
em conta todas as restrições existentes, exige alguma elaboração. Em [Debray 86] e
[Janssens 88] são apresentados algoritmos que produzem bons resultados9. No
9Apesar de nenhum deles ter a pretensão de efectuar uma atribuição de registos ideal.
7. Compilação
78
entanto, estes algoritmos têm a desvantagem de aumentar de forma muito sensível o
tempo de compilação, pois fazem uma análise relativamente complexa da cláusula a
ser compilada, consumindo em particular bastante tempo na detecção e tratamento
prévio dos conflitos.
Como no nosso sistema considerámos o tempo de compilação como um factor a ser
reduzido, eventualmente à custa de uma pequena perca de qualidade do código gerado,
desenvolvemos um algoritmo de atribuição de registos optimista, no sentido em que
não consome demasiado tempo a prevenir-se contra a ocorrência de eventuais
conflitos. Geralmente, só depois da ocorrência destes, é que são tomadas providências
para os resolver. Esta ideia é semelhante à do algoritmo II (conflict resolution) em
[Debray 86], apesar de ser realizada de uma forma diferente, em termos de forma e
conteúdo,
para
evitar
a
manipulação
de
conjuntos
pelo
algoritmo.
Este
é
suficientemente bom para gerar código óptimo nos três exemplos atrás apresentados,
não sendo muito frequente a produção de código de qualidade inferior.
O nosso algoritmo de atribuição de registos começa a actuar logo depois de
terminada a primeira fase da compilação, que corresponde ao preenchimento da
tabela de variáveis. Ele ocupa-se em primeiro lugar da cabeça e do primeiro golo da
cláusula, fazendo um percurso sequencial dos seus argumentos: para cada variável
temporária encontrada a nível superficial na n-ésima posição que ainda não tenha
registo associado, se o registo de ordem n estiver disponível, ele fica imediatamente
adstrito à variável. Desta forma, parte das variáveis temporárias ficam logo com
registo atribuído. Mais tarde, durante a geração de código para a cabeça e para o
primeiro golo da cláusula, sempre que for referenciada uma variável temporária sem
registo associado, é-lhe feita simplesmente a atribuição do registo livre de número de
ordem mais baixo.
As situações de conflito ocorrem, quando se torna necessário usar um registo para
passagem de um parâmetro na derivação de um golo, e ele está ligado a uma variável
temporária cujo valor não se pode perder, pelo facto de ser usada ainda mais à frente.
Nesse caso, a variável temporária tem de ser associada a um registo diferente, e o seu
conteúdo transferido para este registo, o que é feito por geração da instrução PutValue
Xi, Xj. Para a escolha do novo registo Xj ser efectuada, é feita uma análise do primeiro
golo da cláusula com o objectivo de determinar se essa variável ocorre mais à frente ao
primeiro nível dos argumentos do golo. Se assim for, é escolhido o registo
correspondente à sua posição no golo; caso contrário, usa-se simplesmente o registo
disponível que tenha menor ordem. No primeiro caso, a escolha pode
por
coincidência, incidir sobre um registo já atribuído a uma variável temporária, o que
7. Compilação
79
provoca nova situação de conflito, que é resolvida aplicando recursivamente o
algoritmo.
No tratamento dos golos da cláusula a partir do segundo não surgem conflitos, o
que simplifica bastante as coisas. Para cada golo procede-se à anulação das
associações entre registos e variáveis que venham de trás, e analisam-se os seus
argumentos para a atribuição de registos às variáveis temporárias que ocorram
isoladas ao nível da superfície. As outras variáveis temporárias são tratadas quando
forem referenciadas pela primeira vez pelo compilador, durante a fase de geração de
código, sendo ligadas a registos que estejam disponíveis na altura.
7.6. Golos que mudam de contexto
7.6.1. Passagem de parâmetros para units
Na geração de código para fórmulas de extensão, houve o cuidado de reduzir ao
mínimo os inconvenientes causados ao algoritmo de atribuição de registos pelos
padrões de código gerado para as mudanças de contexto. Para isso, foi decidido que na
passagem de argumentos para os designadores de unit não fosse obrigatório usar os
registos da CxM a partir do registo X0, como se faz nas invocações de predicados, mas
antes que existisse a liberdade de usar um conjunto consecutivo de registos qualquer.
Desta forma os designadores de unit nas fórmulas de extensão não se tornam
obstrutivos à introdução de optimizações no golo em que aparecem.
Por exemplo na cláusula
a(X,Y) :- u(Y)>>b(X).
se a variável Y tivesse de ser passada no registo X0 para a unit u, o prejuízo seria
grande pois, não podendo X0 ser ligado à sua variável natural X, seríamos obrigados a
efectuar múltiplas transferências entre registos no código gerado. Mas existindo a
possibilidade de passar Y no registo X1, tudo se simplifica de tal forma, que não chega
mesmo a ser necessário fazer qualquer transferência entre registos. O código
produzido neste caso reduz-se ao seguinte:
PushContext u 1 0
Call b/1
PopContext
Proceed
no qual o segundo argumento de PushContext indica que os parâmetros actuais da
unit, são passados usando os registos a partir de X1 inclusivamente.
Esta regra para a passagem de parâmetros em designadores de units aplica-se da
mesma forma a mudanças de contexto provocadas por invocação de nomes de
predicado importados.
7. Compilação
80
7.6.2. Tratamento de referências a parâmetros de units
Um outro problema que as mudanças de contexto originam tem a ver com o código
de tratamento de referências a parâmetros de units, que sejam efectuadas dentro de
fórmulas de extensão. Consideremos a seguinte definição parcial, da unit u que
contêm o predicado a/2:
unit u(P).
…
a(X,Y) :- v(Y)>>b(X,P).
…
À primeira vista o código gerado para a cláusula parece dever ser o seguinte
(compare com o exemplo anterior):
PushContext v 1 0
PutValue Z0 X1
Call b/1
PopContext
Proceed
No entanto este código não está correcto pois a instrução PutValue Z0 X1 será
sempre executada após concretizada a mudança de contexto, numa situação em que Z0
já não refere o parâmetro P, como pretendido, mas sim o primeiro parâmetro da unit
v. Este problema pode ser facilmente resolvido pela transferência de Z0 para um
registo antes de ser feita a mudança de contexto. O código corrigido fica assim:
PutValue Z0 X2
PushContext u 1 0
PutValue X2 X1
Call b/1
PopContext
Proceed
Três outras situações em que este problema ocorre também a propósito do
parâmetro de unit P são indicadas seguidamente:
unit u(P).
…
a(X,Y) :- u(Y) >> v(P) >> b(X).
a(X) :- < b(X,Y,P).
a(X) :- ? b(X,P).
…
Quando o compilador processa golos cuja execução possa de alguma forma provocar
uma mudança de contexto, ele faz um percurso preliminar completo de todas as partes
dos golos afectadas pela mudança, gerando instruções de transferência para registos
livres, dos valores de todos os parâmetros de unit que sejam referidos. Esses registos
desempenham depois o papel de parâmetros de unit em todo o código gerado para o
golo, mesmo nas partes não afectadas pela mudança de contexto para máxima
eficiência.
7. Compilação
81
7.7. Indexação
Neste ponto são discutidos os mecanismos de indexação usados na CxM. Eles
resultaram de uma extensa revisão do método usado na WAM, que foi adaptado às
exigências do nosso sistema. Para facilitar a compreensão das ideias apresentadas
neste subcapítulo, vamos utilizar diversas vezes como exemplo, um predicado - a/1 com a seguinte definição:
a(1).
a(2).
a(1).
a(3).
a(1).
a(X) :- write(var).
a(2+3).
a(2).
7.7.1. Índice
O objectivo do índice é o de reduzir o tempo dispendido em retrocesso superficial
(shallow backtraking) durante a execução dos programas. Isso consegue-se através de
uma filtragem das cláusulas de um predicado candidatas a emparelhamento, após
cada chamada. Em geral, podem ser ignoradas todas aquelas em relação às quais é
possível garantir antecipadamente falhanço nas unificações das cabeças das
cláusulas, com base nos valores dos argumentos da chamada.
No esquema de indexação da CxM, a selecção das cláusulas é efectuada, tal como na
WAM, apenas com base no valor do primeiro dos argumentos da chamada. Por esse
motivo o registo X0 da máquina é denominado de registo de indexação.
Antes de entrarmos nos aspectos práticos da construção dos índices na CxM,
vejamos primeiro a forma como no código de um predicado, é feita a ordenação das
suas diversas cláusulas constituintes. Conforme se exemplifica seguidamente usando
o predicado a/1, isso é feito, fazendo preceder o código de cada cláusula, por uma
instrução da família TryMeElse/RetryMeElse/TrustMe.
a/1:
C1a:
C1:
C2a:
C2:
C3a:
C3:
C4a:
C4:
C5a:
C5:
C6a:
C6:
TryMeElse C2a
GetConstant 1 X0
Proceed
RetryMeElse C3a
GetConstant 2 X0
Proceed
RetryMeElse C4a
GetConstant 1 X0
Proceed
RetryMeElse C5a
GetConstant 3 X0
Proceed
RetryMeElse C6a
GetConstant 1 X0
Proceed
RetryMeElse C7a
PutConstant “var” X0
write/1 ; predicado de sistema
Proceed
7. Compilação
C7a:
C7:
C8a:
C8:
82
RetryMeElse C8a
GetStruct +/2 X0
UnifyConstant 2
UnifyConstant 3
Proceed
TrustMe
GetConstant 2 X0
Proceed
A cadeia de instruções TryMeElse/RetryMeElse/TrustMe, é percorrida em tempo de
execução, no caso em que X0 contém uma referência a uma variável livre (a qual não
permite que seja efectuada qualquer filtragem), ou quando, por algum motivo, não foi
construído um índice. Com ela, todas as cláusulas alternativas de um predicado são
consideradas na ordem correcta. Quando ocorre a adição ou remoção de cláusulas de
um predicado, a cadeia que lhe corresponde é fácil de alterar, pois o código das
cláusulas não precisa de se encontrar numa zona contígua de memória.
7.7.2. Índice rápido
A ideia subjacente ao esquema principal de indexação da CxM - índice rápido - é
muito simples. Baseia-se na determinação de todas as possíveis sequências de
cláusulas a serem percorridas em tempo de execução, em função do valor do registo X0.
As cadeias de cláusulas são traduzidas pelo compilador em sequências de
instruções Try/Retry/Trust, sendo a selecção da sequência correspondente ao valor de
X0 efectuada por instruções especiais. O índice rápido inicia-se sempre pela instrução
SwitchOnTerm V, C, L, S, que efectua um salto para uma das etiquetas V, C, L, ou S
consoante o tipo do valor de X0, seja respectivamente uma variável, uma constante,
uma lista não vazia ou uma estrutura. Entretanto se no primeiro argumento das
cláusulas de um predicado aparecerem pelo menos duas constantes diferentes e/ou
duas estruturas com functores diferentes, são ainda usadas, respectivamente, as
instruções SwitchOnConstant Tablesize Table Else, e SwitchOnStruct Tablesize Table
Else, as quais permitem determinar rapidamente o início da cadeia de cláusulas
associada a um valor de X0, usando tabelas de hash.
Eis o índice rápido para o nosso predicado a/1 (note que as etiquetas Ci estão
definidas acima, no código das cláusulas do predicado).
a/1_idx:
L1:
T1:
V1:
V2:
V3:
SwitchOnTerm C0, L1, C6, L2
SwitchOnConstant 4, T1, C6
1: V1,
2: V2,
3: V3
Try C1
Retry C3
Retry C5
Trust C6
Try C2
Retry C6
Trust C8
Try C4
Trust C6
7. Compilação
L2:
83
Try C6
Trust C7
Neste código podemos observar que existem 4 cadeias de cláusulas (etiquetadas por
V1, V2, V3 e L2), sendo a mais extensa a que corresponde à constante 1. Esta cadeia, que
surge no código etiquetada por V1, é constituída pelas cláusulas de ordem 1, 3, 5, e 6.
Outro aspecto que merece referência é o facto de a cláusula nº 6 aparecer em todas as
cadeias: isto é resultado do seu argumento ser uma variável.
Só as cadeias de cláusulas não singulares são são representadas por instruções
Try/Retry/Trust. As cadeias constituídas por uma única cláusula são tratadas
mediante um simples salto para o início do seu código dentro do predicado. Isto é o que
se passa com as referências à etiqueta C6 nas duas primeiras instruções do índice.
Entretanto as referências a cadeias de cláusulas vazias são representadas pelo
endereço da instrução Fail. Este é um caso que não aparece no nosso exemplo visto
nele intervir uma variável.
7.7.3. Índice compacto
A técnica de indexação que acabou de ser descrita foi a mais eficiente que
conseguimos conceber com base apenas no registo X0, sendo por isso usada na CxM na
maioria dos casos. Mas existem algumas circunstâncias em que é desaconselhável o
seu uso, por se verificar a criação de índices excessivamente grandes. Isso acontece
para predicados com numerosas cláusulas, nas quais o primeiro argumento apresenta
valores muito diversificados, além de diversas ocorrências de variáveis. Recordemos
que para cada valor diferente do primeiro argumento é criada uma cadeia distinta de
instruções do tipo Try/Retry/Trust, e ainda que as cláusulas com variáveis ocorrem
em todas essas cadeias.
Quando uma situação destas é descoberta pelo compilador, este constrói um índice
de outro tipo, que é um pouco menos rápido, mas, em contrapartida,
é
significativamente mais compacto. Partindo do esquema de indexação descrito no
ponto anterior, a ideia que seguimos foi a de por em evidência todas as referências a
cláusulas que tenham uma variável por primeiro argumento. Para isso o compilador
define uma partição na sequência de cláusulas, na qual cada subsequência obedece à
seguinte disjunção de condições: ou é uma sequência constituída por uma única
cláusula com variável no primeiro argumento, ou então é uma sequência máximal de
cláusulas não tendo variáveis por primeiro argumento. As diferentes subsequências
da
partição
são
exploradas
usando
basicamente
instruções
TryMeElse/RetryMeElse/TrustMe sendo cada subsequência tratada a nível interno
pelo método anterior. Neste esquema a primeira instrução do índice é sempre
JumpOnVar Label que testa se X0 é uma variável livre, caso em que é dado um salto
para o início do código do predicado, que é identificado pela etiqueta Label.
7. Compilação
84
No caso do predicado a/1, que temos vindo a usar como exemplo, existem as
seguintes três subsequências de cláusulas: [1,2,3,4,5], [6], [7,8]. O código gerado pelo
compilador, se ele tomasse a opção de usar este tipo de índice, o que neste caso não é
verdade, seria o seguinte:
a/1_idx2: JumpOnVar C0
T1:
V1:
TryMeElse L1
SwitchOnConstant 4, T1, fail
1: V1,
2: C2,
3: C4
Try C1
Retry C3
Trust C5
L1:
Retry C6
L2:
TrustMe
SwitchOnTerm internal_error, C8, fail, C7
No tratamento da segunda subsequência, pelo facto ela se resumir a uma única
cláusula, não foi necessário seguir o caso geral, que prevê a utilização da instrução
RetryMeElse seguida do código da subsequência: no caso presente esse código
resumir-se-ia a um salto para a etiqueta C6, pelo que a instrução Retry C6 pode ser
usada com vantagem.
Este índice é menos rápido que o anterior, porque durante a sua exploração é
efectuado geralmente um maior número de operações de manipulação de pontos de
escolha as quais são das mais pesadas operações da CxM. Note-se inclusivamente que
em muitas circunstâncias podem existir em simultâneo dois pontos de escolha activos
da responsabilidade de um mesmo índice.
7.7.4. Índice super-rápido
Em alguns casos particulares, mas que ocorrem com frequência em zonas
críticas10 de programas CxLP, o compilador usa duas instruções de indexação, que são
especializações da instrução SwitchOnTerm, e que permitem efectuar ganhos
significativos de velocidade. São as instruções SwitchOnList e SwitchOnFunctor.
A instrução SwitchOnList é usada em substituição da instrução SwitchOnTerm no
índice de todos os predicados que tenham as seguintes duas características: possuir
uma única cláusula com uma lista não vazia por primeiro argumento, e possuir uma
única cláusula com uma constante por primeiro argumento, sendo essa constante o
átomo []. É admitida a existência de mais cláusulas, mas estas terão de ter variáveis ou
estruturas no primeiro
argumento.
Predicados
iteradores
sobre
frequentemente esta forma.
10Uma zona crítica, é um segmento de código de execução muito frequente.
listas
têm
7. Compilação
85
A primeira instrução da cláusula que tem o átomo [] por primeiro argumento, é
sempre GetNil X0 e a primeira instrução da cláusula que contém a lista, é GetList X0 .
Estas duas instruções repetem muitos testes e acções efectuadas pela instrução
SwitchOnTerm, o que explica a criação da instrução SwitchOnList, já que esta
combina a funcionalidade de SwitchOnTerm, GetNil X0 e GetList X0 . Assim quando a
instrução SwitchOnList passa o controlo para uma das duas cláusulas especiais, ela
salta por cima da primeira instrução, ignorando-a simplesmente.
A instrução SwitchOnFunctor tem características semelhantes às da instrução
SwitchOnList com a diferença de se aplicar a outros fuctores que não o functor
ponto “./2”, e de não impor qualquer restrição em relação às constantes no primeiro
argumento.
O ganho que esta pequena optimização proporcionou foi de cerca de 20% no caso do
predicado append/2, testado num computador Vax.
7.7.5. Na WAM
A maior parte das técnicas de indexação usadas na CxM, e que foram atrás
apresentadas, constituem adaptação de ideias já presentes na WAM, sendo importante
referir que, por exemplo, o nosso índice compacto é semelhante aos índices da
máquina de D. Warren. A única diferença essencial entre eles reside no facto de na
WAM, as instruções de indexação serem embutidas pelo compilador no próprio código
das
cláusulas,
ficando
misturadas
com
as
instruções
TryMeElse/RetryMeElse/TrustMe, o que favorece ainda mais a compacidade do
código. No entanto a flexibilidade de alteração dos predicados fica por causa disso
muito comprometida, já que a adição ou remoção de cláusula impõe a recompilação
integral do predicado.
7.8. Compilador optimizador
A presente versão do sistema (CxLP v0.9) não inclui um compilador que faça
análise global de programas. Um tal programa seria útil para processar os programas
que já tenham atingido a sua forma final, e se preparam para ser usados pela
comunidade de utilizadores. No entanto foi já previsto o suporte para um tal eventual
compilador que admite-se, será escrito em CxLP.
Um compilador optimizador precisa de fazer uma distinção entre predicados
dinâmicos e predicados estáticos, pois as optimizações que ele efectua baseiam-se na
circunstância de estes últimos não serem modificáveis durante a execução dos
programas.
Em relação aos predicados estáticos, o compilador optimizador, depois de para
cada um deles analisar a definição e condições de utilização, compila as suas
cláusulas gerando listas de instruções da CxM. Essas cláusulas e respectivo código são
7. Compilação
86
depois adicionadas à base de dados através o predicado de sistema assert/2 (ver
exemplo do utilização no apêndice A). Terminada a compilação de cada predicado, o
compilador declara-o estático, usando a primitiva de sistema static/2. Daí em diante
o predicado compilado não pode sofrer qualquer alteração que não seja a remoção
integral por meio do predicado abolish/2.
Quanto aos predicados dinâmicos, eles poderão ser tratados pelo compilador do
sistema, o qual é invocado usando o predicado assert/1.
7.9. Outras linguagens
No âmbito da linguagem Prolog, é relativamente frequente a utilização de
pré-processamento, com tradução para Prolog, para a prototipagem de certas
linguagens. Este método tem a grande vantagem de constituir a mais rápida forma
disponível para implementar uma nova linguagem, mas tem a desvantagem de o
sistema resultante não ser muito prático de usar. Efectivamente o sistema torna-se
geralmente muito lento na fase de desenvolvimento dos programas, pois ao tempo de
compilação se associa sempre o tempo de pré-processamento; além disso existe o
inconveniente de os programas ao serem listados, não aparecerem na forma original
em que foram introduzidos, mas sim sob a forma de código processado.
Uma alternativa ao uso de pré-processamento, que resolve estes dois problemas, e
que em alguns casos exige um esforço não muito maior de implementação, consiste na
adaptação do compilador existente no sistema CxLP, à geração de código para as
cláusulas da nova linguagem. Isto torna-se especialmente prático se a semântica da
linguagem permitir aproveitar sem alteração, algumas das mais importantes
componentes do compilador, como a parte de gestão da tabela de variáveis ou o
algoritmo de atribuição de registos.
Evidentemente que se a nova linguagem for muito mais potente que a CxLP a
inadequação da máquina CxM vai revelar-se na baixa velocidade de execução dos
programas. Mas existem casos, como por exemplo o do UNL-Prolog [Porto 87], em que
desta forma se pode obter uma implementação plenamente satisfatória da linguagem.
87
8
Outras Componentes do Sistema
8.1. Predicados de sistema
Nesta implementação de CxLP decidimos considerar todos os predicados de sistema
como fazendo parte integrante da própria definição da linguagem. A alternativa seria
situá-los ao mesmo nível dos predicados definidos pelo utilizador, agrupando-os
dentro de uma unit pré-definida, de onde teriam de ser importados antes de poderem
ser utilizados. Foram várias as razões da opção tomada:
• Poupa-se o trabalho de escrita das declarações de importação, que seriam
sempre grande número;
• Dispensa a utilização do mecanismo de importação de predicados, que em CxLP
é uma operação com alguns custos de eficiência, por envolver uma mudança de
contexto;
• Os predicados de sistema comportam-se como estando definidos localmente em
todas as units, o que simplifica a sua utilização em relação ao método
alternativo, no qual teríamos de considerar sempre o facto de a sua execução ser
feita num contexto diferente: eventualmente muitos teriam de ser chamados,
memorizando o contexto corrente com o operador de salvaguarda de contexto
“>”;
8. Outras Componentes do Sistema
88
• Com esta opção a CxLP torna-se uma extensão directa do Prolog podendo
executar directamente os seus programas11.
A lista completa dos predicados de sistema da versão corrente da CxLP, encontra-se
em apêndice para tornar mais prática a sua consulta. Muitos deles são adaptações dos
predicados que se encontram definidos na referência clássica [Clocksin 81], os quais
geralmente se consideram constituir o núcleo básico de predicados de sistema de
qualquer implementação de Prolog. Dentro dessa linha tentámos definir também um
conjunto de predicados reduzido, mas suficientemente completo, adequado à
linguagem CxLP. Os novos predicados introduzidos relacionam-se principalmente
com a manipulação de units e contextos.
Descrevemos seguidamente de forma sucinta os aspectos mais relevantes da
implementação dos predicados de sistema.
8.1.1. Predicados deterministas escritos em C
Recordemos que na técnica de interpretação usada na máquina CxM - código
threaded - o código gerado é constituído essencialmente por uma sequência de
endereços das funções que definem as instruções da máquina abstracta. Ora como
cada predicado de sistema determinista, escrito em C, está também implementado
numa função, surge imediatamente a ideia de inserir directamente o seu endereço no
código gerado.
Esta foi a solução adoptada. Tem a particularidade interessante de colocar os
predicados de sistema ao mesmo nível das instruções da máquina abstracta, com os
benefícios de eficiência que daí advêm. Entretanto este método obriga a que, na
programação dos predicados de sistema, tenham de ser tomados os mesmos cuidados
que são tomados na programação das instruções da máquina abstracta, ou seja: as
funções que definem os predicados não têm argumentos, não têm variáveis locais, e
terminam com a macro JumpNext() (cf. 6.3.4).
8.1.2. Predicados de sistema não deterministas escritos em C
Alguns dos mais importantes predicados de sistema não deterministas são escritos
parcialmente em C, sendo o controlo do não-determinismo feito com código CxM. As
sequências de instruções utilizadas para esse efeito são extremamente eficientes, mas
infelizmente não podem ser geradas pelo compilador a partir de qualquer definição de
predicado escrito em CxLP, por corresponderem a padrões de código considerados à
partida como absurdos. Por esse motivo para cada predicado destes é usada uma
definição aproximada escrita em CxLP, incluída no ficheiro de inicialização do
11Na prática isto não é bem assim pois o Prolog não é uma linguagem estandartizada. Quando
se transporta um programa Prolog de uma implementação para outra é muitas vezes
necessário fazer algumas alterações.
8. Outras Componentes do Sistema
89
sistema CxBoot, sendo efectuada, depois da compilação, uma pequena correcção ao
código gerado.
Este é um pequeno truque que permite duplicar a eficiência do predicado repeat/0 e
que foi usado também nos predicados: clause/2, retract/1, visible_predicate/1,
invisible_predicate/1,
imported_predicate/2,
e
builtin_predicate/1.
Vamos
exemplificá-la com os predicados repeat/0 e clause/2.
repeat/0
A definição do predicado repeat/0 que se encontra no ficheiro CxBoot e que se
encontra obviamente errada, é a seguinte:
repeat.
repeat.
Desta forma o compilador ao processar este predicado gera o seguinte código
máquina:
repeat/0:
L:
TryMeElse L 0
Proceed
TrustMe 0
Proceed
Depois da cláusula compilado o predicado, a primeira instrução da segunda cláusula é
então substituída pela instrução de RetryMeElse, resultando o código:
repeat/0:
L:
TryMeElse L 0
Proceed
RetryMeElse L 0
Proceed
que já corresponde a um repeat/0 correcto.
Note-se entretanto que a utilização da definição habitual:
repeat.
repeat :- repeat.
resulta em código de muito inferior qualidade:
repeat/0:
L:
TryMeElse L 0
Proceed
TrustMe 0
Execute repeat/0
Com efeito neste segundo método, por cada iteração são executadas quatro instruções
(em vez de duas) e é criado e destruído inutilmente um ponto de escolha.
clause/2
O predicado de sistema clause/2 tem de manter um estado interno que lhe permita
determinar a próxima solução a gerar. De entre as vantagem que o método de
implementação do repeat/0 mostrou, existe uma da qual tiramos partido para
resolver o problema: trata-se do facto de ser usado sempre o mesmo ponto de escolha
durante toda a actividade do predicado. A nossa solução passa pela adição de novos
campos ao ponto de escolha para funcionarem como variáveis de estado, o que se
consegue facilmente pela adição de argumentos artificiais ao predicado associado.
8. Outras Componentes do Sistema
90
A definição de clause/2 que se encontra no ficheiro de inicialização do sistema usa
dois predicados auxiliares - $_clause/2 e $_clause/3 - o primeiro dos quais está
escrito em C:
clause(H,B) :- $_clause(H,B,[]).
$_clause(H,B,_) :- $_clause(H,B).
$_clause(H,B,_) :- $_clause(H,B).
Após a compilação destas definições, o código gerado para $_clause/3 é sujeito a
uma alteração interna semelhante à que foi efectuada para o predicado repeat/0, o que
converte aquele predicado também num gerador infinito.
O terceiro argumento de $_clause/3 é artificial, funcionando como variável de
estado do predicado $_clause/2 cujo estado inicial é representado pelo átomo [].
Quando não há mais soluções a gerar, a instrução da CxM DiscardAndFail é chamada
do interior de $_clause/2, sendo anulado o ponto de escolha.
8.1.3. Predicados de sistema escritos em CxLP
Por uma questão de conveniência muitos predicados de sistema foram escritos
totalmente em CxLP, como por exemplo: consult/1, reconsult/1, listing/0, listing/1, e
all/0. Todos os predicados carregados na unit [] no momento em que é executado o golo
“$_protect_all” passam a ser considerados predicados de sistema. Este golo é
executado automaticamente logo após o carregamento do ficheiro inicialização,
CxBoot.
8.2. Analisador sintáctico
Uma componente importante do sistema CxLP é o analisador sintáctico, que serve
para fazer a tradução de representações textuais de termos nas suas representações
internas. A sintaxe suportada é a dita de Edimburgo (usada habitualmente no Prolog),
não tendo sido necessário introduzir grandes
modificações
motivadas
pelas
características especiais da CxLP. Realmente a única alteração registada foi a da
adição de suporte para a detecção de referências a parâmetros de units, nos termos
lidos.
A sintaxe Edimburgo é definida por uma gramática pertencente à categoria das
gramáticas de operadores [Aho 86], que são conhecidas pela relativa facilidade de
escrita de parsers do tipo shift-reduce eficientes para elas. A definição do analisador
sintáctico, que neste caso recebe o nome de parser de precedência de operadores, passa
pela definição de relações de precedência entre a maior parte dos símbolos terminais
da gramática. Os símbolos usados para designar essas relações são os símbolos, “<”,
“>”, e “=”.
O parser funciona da seguinte forma: à medida que vai analisando o texto de
entrada da esquerda para a direita, vai empilhando todos os símbolos terminais até
que apareca um par relacionado por >; então começa a descer na pilha até encontrar
8. Outras Componentes do Sistema
91
um par de símbolos terminais relacionados por <; o conjunto de terminais delimitado
por < e > é reduzido; e o processo continua até que o termo em análise tenha sido
reduzido no seu todo.
A parte mais importantes da definição do analisador consiste na determinação da
(eventual) relação de precedência entre cada par de símbolos. Para os pares que não
envolvem operadores, esta relação tem carácter estático, podendo ser definida num
quadro de duas entradas. No caso de dois operadores de diferentes prioridades, op1 e
op2, se op1 tiver prioridade superior a op2 então a relação de precedência é a inversa,
isto é op1< op2. Finalmente para dois operadores de igual prioridade, a relação de
precedência é determinada com base nas suas associatividades. As regras relativas a
este caso podem também ser codificadas numa tabela de duas entradas.
No nosso sistema seguimos muito de perto a descrição pormenorizada de um
analisador sintáctico para Prolog, que se encontra na referência [Kluzniak 85] (págs.
203-212). Nesta referência encontra-se inclusivamente uma implementação completa
do analisador escrita em Prolog (págs. 264-272). No entanto não usámos esse código
directamente pois as nossas exigências de eficiência geral do sistema, em particular
do processo de compilação, impunham um analisador escrito completamente em C.
8.3. Tratamento de erros
No decurso da execução de programas CxLP muitos tipos de erros podem ocorrer
sendo assinalados por mensagens apropriadas. Os erros que mais importantes são os
seguintes: divisão por zero, erro na leitura de um termo, chamada de um predicado de
sistema com argumentos errados, falta de memória na área de código, transbordo das
pilhas de execução e execução de uma fórmula de reposição de contexto sem que
previamente tenha sido salvaguardado qualquer contexto. No nosso sistema todos
estes erros, com excepção dos erros de sintaxe, são fatais pois provocam a imediata
reinicialização da máquina e o relançamento do monitor de comandos.
Para proporcionar às aplicações CxLP algum controlo sobre situações de erro, foi
definida a primitiva de sistema error_handler/1, que permite indicar um predicado
alternativo ao monitor de comandos. No entanto, atendendo a que nessa altura todo o
contexto do erro se encontra já irremediavelmente perdido, geralmente esse predicado
pouco mais poderá fazer de útil, do que actuar como um monitor de comandos
específico da aplicação.
8.4. Colector de lixo do heap
Na CxM são mantidas todas as técnicas inerentes ao funcionamento da WAM que
têm por objectivo manter um certo controlo sobre o consumo de memória, e que são as
seguintes:
8. Outras Componentes do Sistema
92
• Repartição dos dados criados em tempo de execução por duas diferentes áreas stack local e heap - para possibilitar recuperação de espaço no stack local
durante a exploração dos ramos deterministas das árvores de derivação.
• Utilização de LCO (last call optimization) para incrementar o volume de
memória recuperado no stack local durante as fases deterministas de execução
dos programas.
• Recuperação de memória no stack local, heap e trail sempre que ocorre
retrocesso.
• Não memorização no trail das variáveis que vão deixar de existir quando for
feito retrocesso para o ponto de escolha corrente.
• Recuperação de memória no stack local e trail resultante da execução do
predicado cut.
Um aspecto importante do esquema apresentado, é o facto de a recuperação de
espaço do heap só ser efectuada em retrocesso, o que implica que os dados incluídos no
heap tenham em geral um tempo de vida bastante superior à dos dados contidos no
stack local. Não é pois de surpreender que, durante a execução dos programas, uma
parte importante do conteúdo do heap seja constituída por informação redundante.
Assim nos casos em que a ocupação do heap se aproxima do seu limite máximo, é
importante que sejam tomadas medidas para nos libertarmos do lixo nele contido, já
que a alternativa seria a de o programa não poder continuar a correr. O problema da
detecção dos termos redundantes e posterior compactação do heap, é um problema
complexo mas bastante bem estudado no caso da WAM, existindo mesmo descrições
publicadas de alguns algoritmos. Em geral nestes algoritmos distinguem-se duas
fases: na primeira fase é efectuada uma busca exaustiva de todos os termos acessíveis
os quais são marcados de uma forma especial; na segunda fase é feita a compactação
do heap sendo eliminados todos os termos não marcados. A adaptação de qualquer
destes algoritmos à CxM não é muito complicada pois basta considerar, na
determinação dos termos acessíveis, os novos objectos presentes no stack local: as
instâncias de unit e os registos históricos.
A escrita do módulo de colecta de lixo da CxM, que só por si é suficientemente
complexa para constituir um projecto de programação autónomo, está em fase de
desenvolvimento pelo aluno finalista de Eng. Informática e bolseiro António Páscoa.
O algoritmo que está a ser usado como base do trabalho é o descrito na referência
[Appleby 88]. Eventualmente será tirado bom partido dos mecanismos aí descritos
para recolha de lixo segmentada, os quais permitem considerar individualmente
segmentos do heap delimitados implicitamente
por
dois
pontos
de
escolha
consecutivos. Esperamos que a utilização desta técnica, orientada por uma cuidadosa
8. Outras Componentes do Sistema
93
política de aplicação, permita reduzir consideravelmente o tempo total dispendido
nesta tarefa.
8.5. Colector de lixo da área de código
A eliminação de cláusulas usando por exemplo a primitiva retract/1 levanta um
problema importante: em geral quando uma cláusula é removida, o espaço de
memória por ela ocupado não pode ser imediatamente reaproveitado, pois existe a
possibilidade de ela estar ligada a uma chamada de predicado ainda activa ou ser
reactivada após retrocesso.
Outro problema semelhante a este, ocorre quando um índice ou um segmento de
código de ligação de nome importado é invalidado. O espaço ocupado também não pode
ser libertado imediatamente, pois também eles podem estar envolvidos numa
chamada ainda activa.
Estes problemas foram resolvidos por recurso a um colector de lixo adaptado às
circunstâncias. O colector de lixo da área de código recorre a uma tabela onde vão
sendo memorizados os endereços dos objectos eliminados durante a execução dos
programas. Em condições de falta de memória - tabela cheia ou área de código
esgotada -, o algoritmo percorre os pontos de escolha e registos de activação que se
encontram no stack local, obtendo todas as referências neles feitas a código CxM.
Estas referências são depois usadas para determinar de entre os objectos eliminados,
todos aqueles que são, directa ou indirectamente, acessíveis. O espaço ocupado pelos
objectos não acessíveis é então libertado.
Além das duas ocasiões limite de falta de memória que referimos, este algoritmo é
também ocasionalmente executado automaticamente pelo monitor de comandos do
sistema, em situações em que o nível de ocupação do stack local é baixo, o que permite
uma rápida execução do algoritmo.
8.6. Monitor de Comandos
O monitor de comandos (top level) do sistema segue o estilo de funcionamento
habitual de um interpretador de Edimburgo, com algumas pequenas adições. O prompt
habitual é o seguinte:
[…] ?-
onde a lista indica o contexto corrente em que o monitor de comandos está a correr.
Geralmente essa lista é vazia o que indica que o contexto corrente é vazio, e que a unit
corrente é a unit []. Existem dois comandos para controlar o contexto em que corre o
monitor de comandos: o comando “push u” que cria um nova instância do monitor,
correndo no contexto estendido por u, e o comando “pop” que permite regressar à
instância anterior.
94
9
Conclusão
Acabámos de descrever neste trabalho, um sistema de programação em lógica
contextual (CxLP v0.9) que estende directamente a linguagem Prolog. Nele se
demonstra a possibilidade de implementar com razoável eficiência os mecanismos
específicos da programação em lógica contextual, e de forma a praticamente não
comprometer a velocidade de programas escritos em Prolog.
O sistema já se encontra praticamente operacional, sendo bastante agradável de
usar devido à sua funcionalidade externa, indistinguível da de um interpretador, e à
sua boa velocidade de resposta em todas as situações, incluindo o arranque do
sistema, a compilação de cláusulas e a execução de programas.
O código fonte é bastante extenso compreendendo cerca de 14000 linhas de 20
caracteres em média (excluindo comentários). Cerca de metade deste código destina-se
ao suporte dos novos mecanismos da linguagem CxLP, o que dá uma boa medida do
acrescido trabalho que a sua implementação dá, em relação ao Prolog. É importante,
no entanto, salientar que em termos de dificuldade intrínseca de implementação, ser
correcto afirmar que a linguagem CxLP se situa dentro do mesmo nível de magnitude
que o Prolog.
O número de versão do sistema actual é 0.9, não sendo portanto esta, ainda, a sua
primeira versão oficial. Com efeito falta ainda terminar o desenvolvimento do
colector de lixo do heap e desenvolver um debugger usando o modelo clássico das
quatro portas de Byrd (cf. [Clocksin 81]). Algumas adições importantes a fazer depois
disso, serão: suporte para database references, ampliação do conjunto de predicados
de sistema tomando por referência o standard da indústria Quintus-Prolog, criação de
9. Conclusão
95
biblioteca de programas utilitários, suporte para gramáticas de cláusulas definidas, e
criação de facilidades para armazenamento e carregamento de programas já
compilados (fast save & load).
Existem, entretanto, algumas componentes do sistema cujo aperfeiçoamento ainda
é possível, como é o caso do compilador, para melhoria do código gerado (desde que a
velocidade de compilação não seja muito comprometida), ou da máquina abstracta,
para aumento da sua velocidade (por modificação ou criação de novas de instruções: a
ideia que está na base do índice super-rápido, por exemplo, tem um campo mais vasto
de aplicação).
96
Apêndices
97
A
Predicados de sistema
A.1. Introdução
Apresentamos aqui exaustivamente os predicados de sistema da versão 0.9 do
sistema CxLP. Muitos deles correspondem basicamente aos predicados de sistema
tradicionais do Prolog, mas no contexto da linguagem CxLP a operacionalidade de
alguns teve de sofrer algumas modificações. No que respeita aos predicados
completamente novos, a maior parte destina-se a fazer o controlo das units e seu
conteúdo e a fornecer mecanismos para meta-programação usando contextos. Um
predicado especialmente útil é o predicado chk/0”, que simula a fase de ligação dos
ambientes de desenvolvimento de programas tradicionais com vista à detecção prévia
de situações anómalas. As futuras versões de CxLP fornecerão um conjunto de
predicados mais rico, eventualmente compatível com o Quintus-Prolog.
No ponto
8.1
são
descritos
os
aspectos
mais
relevantes
relacionados
a
implementação dos predicados de sistema.
A.2. Sucesso e falhanço
true
Sucede sempre.
fail
Falha sempre.
A.3. Controlo
! (cut)
Congela todas as alternativas criadas após a chamada do
predicado onde ocorre. Só funciona de forma estática quando
embebido no código das cláusulas. Se for usado de forma
interpretada como na cláusula:
a :- X = !, X.
A. Predicados de sistema
98
surge a mensagem: “Interpreted 'cut' doesn't work.”. Na maior
parte dos compiladores de Prolog, o cut comporta-se de forma
semelhante ao nosso, com a diferença de a mensagem de aviso
não ser emitida.
repeat
Serve para gerar múltiplas soluções usando retrocesso. A sua
definição poderia ser definida como se indica seguidamente, mas
a nossa implementação usa internamente um processo mais
eficiente:
repeat.
repeat :- repeat.
call(X)
Sucede se e só se X suceder no contexto corrente.
Semelhante ao anterior mas ignora as restrições de visibilidade
unsafe_call(X)
existentes nas units do contexto corrente.
X
Equivalente a call(X).
abort
Reinicializa o sistema passando o controlo para o monitor de
comandos (top level).
halt
Pára o interpretador emitindo a mensagem: “CxLP halted!”.
A.4. Manipulação de golos
X,Y
Conjunção de golos.
X;Y
Disjunção de golos.
not X
Sucede sse X falhar no contexto corrente. Definido em CxLP como:
not X :- X, !, fail.
not _.
\+ X
Idêntico a not X.
check(X)
Sucede sse X suceder, mas não instância as variáveis. Definido
como:
check(X) :- not not X.
side_effects(X)
É apenas uma designação diferente para o predicado
check(X) que deve ser usada quando estamos interessados apenas
nos efeitos laterais de uma derivação.
once(X)
Deriva X deterministicamente. Definido como:
once(X) :- X, !.
U>>X
Deriva X no contexto corrente, estendido pela unit designada por
U.
A. Predicados de sistema
99
Deriva X guardando previamente o contexto corrente no contexto
>X
histórico.
Deriva X desempilhando previamente
<X
o
topo
do
contexto
histórico.
If->Then
Deriva Then sse If suceder. A definição é a seguinte:
If -> Then :- If, !, Then
If->Then;Else Deriva Then ou Else consoante If suceda ou não. Definido como:
If -> Then ; Else :- If, !, Then.
If -> Then ; Else :- !, Else.
findall(X,G,L)
L é a lista de todos os termos da forma X que satisfazem o golo G.
A.5. Gestão das units e do seu conteúdo
Os predicados de gestão da base de dados do Prolog foram modificados em CxLP
para actuarem sobre a unit corrente. Assim, por exemplo, para adicionarmos a
cláusula
“rei(‘Henriques, A.’, Portugal)”.
à
unit
reis,
basta
executar
o
golo
“reis>>assert(rei(‘Henriques, A.’, Portugal)).”. Foram também introduzidos alguns
predicados para controlo de atributos de predicados e para acesso ao conteúdo das
units.
units(L)
Unifica L com a lista de átomos que nomeiam todas as units
correntemente existentes, excepto a unit pré-definida [].
create_unit(U,V)
Cria a unit U caso ainda não exista. U e V são termos
equivalentes aos que resultariam da leitura do termo que
descreve a unit usando o predicado read(U,V). Assim, para criar a
unit
authors(Works),
deve
invocar-se
“create_unit(authors(Works), [Works=‘Works’])”. Em geral não é
necessário usar esta primitiva
directamente
visto
que
os
predicados consult/1 e reconsult/1 a utilizam sempre que se torna
necessário criar uma nova unit.
clear_unit(U) Elimina o conteúdo da unit nomeada pelo átomo U. Não é possível
eliminar totalmente uma unit previamente criada devido a
poderem existir referências a ela noutras units.
asserta(T)
Adiciona a cláusula T no início do seu predicado na unit corrente.
Esta operação
faz
a
compilação
da
cláusula T
e a sua
memorização como termo-fonte.
asserta(T,C)
Semelhante ao predicado anterior, com a diferença de que o
compilador não chega a ser chamado pois o segundo argumento C
já contêm a lista do código a ser usado. Este predicado destina-se a
ser usado por um eventual futuro compilador-optimizador,
A. Predicados de sistema
100
escrito em CxLP. Entretanto pode ser utilizado já em experiências
de escrita em assembler da CxM. É preciso alguma cautela, pois a
utilização de código absurdo tem
muitas
vezes
resultados
catastróficos. Eis um exemplo de utilização:
asserta((a(X,Y) :- a(Y,X)),
[put_value(x0,x2),
put_value(x1,x0),
put_value(x2,x0),
execute(a/2)]).
As instruções são automaticamente especializadas pelo sistema
quando for caso disso.
assertz(T)
Como asserta(T) mas a cláusula é adicionada no fim de seu
predicado.
assertz(T,C)
Como asserta(T,C) mas a cláusula é adicionada no fim de seu
predicado.
assert(T)
Equivalente a assertz(T).
assert(T,C)
Equivalente a assertz(T,C).
assertx(T)
Semelhante a assertz(T), mas só pode ser usado imediatamente a
seguir à leitura do termo T por meio de uma das variantes do
predicado read. É mais rápido que o predicado assertz porque o
compilador, neste caso, usa a tabela de variáveis já construída
pelo analisador sintáctico. Este predicado permite aumentar a
eficiência dos predicados consult/1 e reconsult/1 por exemplo.
H :- B
Adiciona a cláusula indicada à unit corrente. O seu código é:
(H :- B) :- assert((H :- B)),
writep((H :- B)),
writeln(' asserted.').
retract(T)
Elimina cláusulas da unit corrente que unifiquem com T. É
removida uma cláusula por cada tentativa de satisfação com
sucesso. Aplica-se a todos os predicados com cláusulas existentes
na unit corrente, mesmo aos não visíveis.
retractall(T) Elimina todas as cláusulas da unit corrente que unifiquem com T.
abolish(A,N)
O predicado A/N é removido. Também se aplica a nomes de
predicados importados, que são removidos da unit corrente.
clause(H,B)
Permite percorrer, usando retrocesso, todas as cláusulas da unit
corrente cuja cabeça unifique com H e cujo corpo unifique com B.
listing(A/N)
Lista o predicado A/N da unit corrente.
listing(A)
Lista
todos
os
predicado
da
unit
independentemente da sua aridade.
corrente,
chamados
A,
A. Predicados de sistema
listing
101
Lista o conteúdo da unit corrente, sendo assinalados os predicado
visíveis e importados. Para listar, por exemplo, a unit authors
fazer authors>>listing.
all
Lista o conteúdo de todas as units existentes na base de dados da
CxLP.
code(A/N)
Lista o código CxM do predicado A/N da unit corrente. A/N pode
ser um predicado de sistema.
code(A)
Lista o código CxM de todos os predicado da unit corrente,
chamados A, independentemente da sua aridade.
consult(File) Adiciona as cláusulas e declarações de visibilidade e importação
existentes no ficheiro File à unit corrente. Se o ficheiro se iniciar
por um termo da forma “unit U.” então o resto do seu conteúdo é
carregado na unit U, que é criada caso não exista ainda.
Semelhante ao predicado anterior com a diferença que
reconsult(File)
todos os predicados a que corresponda uma cláusula em File são
completamente redefinidos.
visible A/N
novisible(A,N)
O nome de predicado A/N fica visível na unit corrente.
O nome de predicado A/N fica invisível na unit corrente.
import A/N from U
Define o nome de predicado A/N como sendo importado
de U. U pode ser um designador de unit ou um parâmetro da unit
corrente.
noimport(A,N) Anula qualquer declaração de importação que exista a respeito do
nome de predicado A/N.
noindex(A,N)
Indica que a técnica de indexação suportada pela CxM não deve
ser aplicada ao predicado A/N.
protect(A,N)
O predicado A/N deixa de poder sofrer qualquer alteração. Todos
os predicados de sistema estão protegidos desta forma.
static(A,N)
A única alteração que o predicado A/N pode sofrer é a completa
remoção usando o predicado abolish/2.
visible_predicate(T)
Este predicado pode ser usado tanto para teste como para
iterar sobre o conteúdo da unit corrente. Como predicado de teste
sucede se T for um termo da forma P/N correspondente a um
predicado visível na unit corrente. Se T for uma variável, usando
retrocesso, T vai sendo sucessivamente instanciado com pares
P/N, correspondentes aos functores de todos os predicados
visíveis existentes na unit corrente.
A. Predicados de sistema
invisible_predicate(P)
102
O que se disse em relação ao predicado anterior
aplica-se aqui mutatis mutandis a predicados não importados
que não tenham objecto de nenhuma declaração de visibilidade.
imported_predicate(P,U)
Semelhante
ao
anterior.
Note-se
que
os
3
conjuntos de nomes - visíveis, invisíveis e importados - da forma
como são aqui entendidos, constituem uma partição do conteúdo
das unit.
builtin_predicate(P)
Do tipo dos anteriores este predicado
aplica-se
a
predicados de sistema.
user_predicate(P)
Disjunção de visible_predicate(P) com invisible_predicate(P).
any_predicate(P)
Disjunção de user_predicate(P) com builtin_predicate(P).
A.6. Contextos
Os predicados seguintes permitem aceder aos contextos correntes, simples e
histórico, e obter os valores dos parâmetros da unit corrente de forma indexada.
Destinam-se em grande medida à utilização em meta-programação.
context(C)
Unifica C com uma lista representando o contexto lógico corrente
(portanto sem a unit terminal []). Os elementos da lista são todos
designadores de unit.
context_top(T)
Unifica T com o designador da unit corrente. Este predicado foi
definido apenas por razões de eficiência, visto o predicado
context(C) poder ser usado em alternativa.
historic_context(H)
Unifica H com uma lista representando o contexto
histórico corrente. Os elementos de H são listas que representam
contextos.
unit_parameter(N,V)
Unifica V com o valor corrente do parâmetro de ordem N
(>=1) da unit corrente.
make_unit_parameter(N,UP)
Um termo da forma “$_unit_parameter(N)” é
invariavelmente tratado pelo compilador como representando o
parâmetro da unit corrente de ordem N. Se por algum motivo
tivermos necessidade de usar aquele termo como representante de
si mesmo, não podemos simplesmente escrevê-lo no código dos
programas. A solução consiste em criar o termo dinamicamente,
o que evita a sua análise por parte do compilador. Para isso pode
usar-se o predicado:
make_unit_parameter(N,UP) :- UP =.. [$_unit_parameter,N].
A. Predicados de sistema
103
A.7. Classificação de termos
var(X)
Testa se X é uma variável não instanciada.
novar(X)
Testa se X é um termo não variável.
atom(X)
Testa se X é um átomo.
integer(X)
Testa se X é um valor inteiro.
real(X)
Testa se X é um valor real.
number(X)
Testa se X é um valor numérico.
atomic(X)
Testa se X é um átomo ou um valor numérico.
unit_designator(X) Testa se X é um designador de unit. Um designador de unit é
um termo não variável, cujo functor principal é um nome de unit
existente.
A.8. Manipulação de estruturas
functor(T,F,N)
T é uma estrutura com o functor F e aridade N. Se T não estiver
instanciado F e N têm de o estar.
arg(N,T,A)
A é o argumento de ordem N da estrutura T. N e T têm de estar
instanciados.
X =.. L (univ) L é a lista consistindo no functor
de X seguido dos seus
argumentos. Definido em CxLP, usando o predicado auxiliar
$_=../2, como:
T =.. [F|As] :- $_=..(As,T,F,0).
$_=..([],T,F,N) :- functor(T,F,N), !.
$_=..([A|As],T,F,N) :N1 is N+1, $_=..(As,T,F,N1), !, arg(N1,T,A).
name(A,L)
L é uma lista de inteiros constituída pelos códigos dos caracteres
constituintes do átomo L.
A.9. Igualdade
X = Y
Unifica X com Y.
X \= Y
Equivalente a not X = Y.
X == Y
Sucede sse os dois argumentos forem duas ocorrências do mesmo
termo.
X \== Y
Equivalente a not X == Y.
A. Predicados de sistema
104
A.10. Avaliação de expressões aritméticas e comparação de
números
X is Y
Y deve estar instanciado com uma estrutura que possa ser
interpretada como uma expressão aritmética. O valor dessa
expressão é avaliado e unificado com X. O resultado da avaliação é
convertido para inteiro sempre que a sua parte decimal for zero
ou muito próxima. Os operadores disponíveis são: +/2, -/2, */2,
//2, mod/2, cputime/0 e heapused/0. “cputime” indica o tempo
total de CPU que foi usado desde que o sistema arrancou.
“heaptime” indica o espaço total ocupado na zona de código da
CxM (o nome deve-se a uma questão de compatibilidade com o
C-Prolog).
X
X
X
X
X
X
=:= Y
=\= Y
< Y
> Y
>= Y
=< Y
Predicados de comparação de valores numéricos.
A.11. Gestão de ficheiros
Eis os predicados de alteração dos canais correntes de entrada e saída. Neste
momento é apenas suportado o fluxo de dados de e para os ficheiros em memória
secundária. No entanto num sistema de janelas deverá feita a sua adequada
generalização. Existem dois
canais
pré-definidos
ambos
de
nome
user que
representam o teclado do computador e o seu dispositivo de visualização.
see(S)
O canal corrente de entrada passa a ser o designado pelo átomo S.
seeing(S)
S é unificado com o nome da canal corrente de entrada.
seen
Fecha o canal corrente de entrada. O canal user torna-se corrente.
tell(S)
O canal corrente de saída passa a ser o designado por S.
telling(S)
S é unificado com o nome da canal corrente de saída.
told
Fecha o canal corrente de saída. O canal user torna-se corrente.
A.12. Entradas e Saídas
Todos os predicados de entrada e saída que aqui se descrevem são determinísticos.
Estão definidos alguns predicados de leitura e escrita que consideram a necessidade de
reconhecimento do nome dos parâmetros da unit corrente nos termos processados.
read(X)
Unifica X com o próximo termo no canal de entrada corrente. Em
fim de canal é produzido o átomo end_of_file.
A. Predicados de sistema
readp(X)
105
Semelhante a read(X) mas desta vez são considerados os nomes
dos parâmetros da unit corrente, com o objectivo de reconhecer a
sua presença no termo lido. Os parâmetros são representados por
termos
da
forma
“$_unit_parameter(N)”
em que
N(>=1)
representa a ordem do parâmetro lido.
read(X,Y)
Unifica X com o próximo termo do canal de entrada corrente, e Y
com uma lista de pares “V=‘nome’” onde se indica o nome de todas
as variáveis do termo.
readp(X,Y)
Semelhante a read(X,Y) mas sendo feito o reconhecimento dos
parâmetros da unit corrente.
write(X)
Escreve o termo X no canal corrente de saída. Existe um
codificação especial para os nomes das variáveis que surjam. São
respeitadas todas as definições de operadores correntemente
definidas.
writep(X)
Escreve o termo X no canal corrente de saída tal como write(X).
Qualquer termo da forma “$_unit_parameter(N)” é substituído
pelo nome do parâmetro da unit corrente de ordem N, se existir.
write(X,Y)
Semelhante a write(X) mas desta vez são respeitados os nomes das
variáveis que se encontram definidos em Y usando um lista de
pares “V=‘nome’”.
writep(X,Y)
Semelhante a write(X,Y) mas sendo feito o tratamento dos nomes
dos parâmetros da unit corrente.
writeln(X)
writeln(X) :- write(X), nl.
writepln(X)
writepln(X) :- writep(X), nl.
display(X)
Semelhante a write(X) mas desta vez são ignoradas todas as
declarações de operadores existentes.
op(X,Y,Z)
Permite declarar operadores. X é a precedência do operador que se
trata de um valor inteiro entre 0 e 1200, variando a precedência de
forma inversamente proporcional ao seu valor. Y indica a
posição e associatividade do operador, podendo ser um dos
seguintes átomos: fx, fy, xf, yf, xfx, xfy, yfx, yfy. Z é o nome do
operador, ou uma lista de nomes de operador. Este predicado
sucede sse a definição do operador for legal.
O conjunto de declarações que define o conjunto de predicados
pré-definido é o seguinte:
op(1200, fx, [(:-), (?-)]).
op(1200, xfx, (:-)).
op(1100, xfy, (';')).
op(1050, xfy, ('->')).
A. Predicados de sistema
106
op(1000, xfy, (',')).
op( 900, fy, [\+, not, unit, visible,
import, push]).
op( 800, xfy, from).
op( 700, xfx, [=:=, =\=, <, >=, >,
=<, is, =..]).
op( 700, xfx, [ ==, \==, =, \=]).
op( 700, xfy, >>).
op( 700, fy, [ >, <]).
op( 500, yfx, [+, -]).
op( 500, fx, [+, -, \]).
op( 400, yfx, [*, /, div, <<, >>]).
op( 300, xfx, mod).
reset_ops
Repõe o estado inicial da tabela de operadores do sistema, fazendo
portanto a anulação de todas as adições e alterações efectuadas
desde o início da entrada no sistema.
get0(X)
X é unificado com o código do próximo carácter do canal de
entrada. No fim de canal é produzido o valor 26.
get(X)
X é unificado com o código do próximo carácter do canal de
entrada imprimível. No fim de canal é produzido o valor 26.
skip(X)
São ignorados todos todos os caracteres do ficheiro de entrada até
ser encontrado um cujo código unifique com X.
put(X)
Escreve o carácter de código X no canal corrente de saída.
nl
Escreve uma mudança de linha no canal corrente de saída.
tab(X)
Escreve X brancos no canal corrente de saída.
A.13. Diversos
statistics
Exibe diversa informação sobre o estado corrente do sistema,
concretamente a ocupação das diversas zonas de memória da CxM
e o tempo de CPU usado desde que o sistema começou a correr.
status
Exibe informação sobre o conjunto de indicadores de estado da
CxM que são descritos em status/2.
status(I,S)
Serve para controlar o valor de alguns indicadores de estado
existentes na CxM. I é um átomo que selecciona o indicador de
estado em que estamos interessados. S é unificada com o valor
corrente do indicador, podendo ser uma variável ou um dos
átomos on ou off. Os valores possíveis para I são os seguintes:
• specialize
Controla como é feita a especialização de instruções referida
no ponto 6.6. Valor por defeito: on.
• keep_source
Todos os predicados criados durante períodos em que este
indicador
esteja
a
off
não
memorizam
os
termos
correspondentes às cláusulas que os constituem, sendo
A. Predicados de sistema
107
economizada desta forma cerca de 35% de memória. No
entanto este facto impede a aplicação de algumas primitivas
de sistema a estes predicados, concretamente: clause/2,
retract/1 e retractall/1. Valor por defeito: off.
• diagnose_anomalies
Durante a execução dos programas todas as
situações menos normais são
assinaladas,
como
por
exemplo a invocação de um predicado não existente, ou a
pesquisa no contexto corrente provocada por referência a
um predicado importado de uma unit, na qual não está
declarado como visível. Valor por defeito: on.
• import_search
Se estiver off altera um pouco a semântica normal da
chamada de predicados importados, provocando falhanço
imediato
(e
produzindo
uma
mensagem
se
diagnose_anomalies estiver on) sempre o predicado não
esteja declarado como visível na unit de onde é importado.
Valor por defeito: on.
• ctx_ext_search Se estiver off altera um pouco a semântica normal das
fórmulas de extensão, provocando falhanço imediato (e
produzindo uma mensagem se diagnose_anomalies estiver
on) sempre o predicado não esteja declarado como visível na
unit que estende o contexto. Valor por defeito: on.
• indexing
Permite desligar a indexação de todos os predicados criados
enquanto o indicador estiver a off. Valor por defeito: on.
• abort_on_syntax_error Se estiver on o predicado abort/0 é chamado
sempre que ocorrer algum erro sintáctico durante o uso de
alguma das variantes do predicado read.
• trace
Caso esteja on mostra durante a execução dos programas
todas as chamadas de predicados com os respectivos
argumentos e contexto corrente. Neste momento a única
ferramenta para depuração de programas é esta, o que
reconhecemos, não é suficiente. Valor por defeito: off.
chk
Faz uma análise das relações de dependência entre as units
existentes
assinalando
todas
as
referências
a
predicados
importados de uma unit na qual não estejam declarados como
visíveis, e ainda todas as fórmulas de extensão da forma U>>P,
em que P não é visível em U. O objectivo é a detecção de algumas
situações de erro que de outra maneira só poderiam ser detectadas
em tempo de execução. A detecção dinâmica de erros não é segura,
A. Predicados de sistema
108
pois é frequente passarem despercebidos alguns erros, mesmo
após extensivos testes ao programa.
$_sys_chk
Invoca uma rotina, que faz alguns testes sobre a máquina
hospedeira e sobre a própria CxM, com vista a detectar eventuais
problemas e a facilitar o seu transporte.
$_protect_all Protege
todos os predicados de sistema carregados até ao
momento. É chamado após o carregamento do ficheiro CxBoot,
com o objectivo de proteger todos os predicados de sistema.
error_handler(Handler)
O
predicado
que
por
defeito
é
activado
automaticamente quando ocorre um erro de sistema, é o que
corresponde ao monitor de comandos do sistema. Usando o
predicado error_handler/1 é possível indicar um outro predicado
sem argumentos para ser usado em alternativa. A chamada
“error_handler([])” repõe o estado inicial.
109
B
Operações Básicas e Instruções da CxM
B.1. Introdução
Apresenta-se neste apêndice a especificação das instruções da máquina CxM, assim
como das principais operações básicas nela usadas. A procura de concisão e clareza da
descrição levou-nos a eliminar da descrição todos os aspectos que não tivessem a ver
directamente com a essência da funcionalidade da máquina. Os principais aspectos
ignorados foram os seguintes: a interacção das instruções com o ambiente de
desenvolvimento dos programas (debugger por exemplo), os testes de transbordo das
pilhas (estas são consideradas como tendo dimensão infinita) e as múltiplas
optimizações de pormenor introduzidas durante a codificação das instruções.
B.2. Pseudo-código
A descrição de todas as instruções e operações básicas é efectuada numa notação
muito semelhante à da linguagem dos comandos guardados - GCL - de E. Dijkstra
[Dijkstra 76]. Esta linguagem foi escolhida devido à extrema compacidade e elegância
com que permite fazer a especificação de algoritmos.
Em GCL existem duas construções para controlo do fluxo de execução dos
programas: a construção alternativa if … fi e a construção repetitiva do … od. O
interior dessas construções consiste num conjunto de comandos guardados cada um
dos quais tem a forma: “<guarda> → <comando>”. Um comando guardado só pode ser
executado se a sua guarda for verdadeira. Entretanto se várias guardas de um mesmo
B. Operações Básicas e Instruções da CxM
110
conjunto de comandos guardados forem verdadeiras simultaneamente, a escolha
entre elas é efectuada de forma não determinista.
A instrução skip representa a instrução vazia. A afectação de variáveis é indicada
usando o operador “←”. A linguagem suporta todos os operadores lógicos e relacionais
habituais.
A semântica que atribuímos à construção if … fi é aqui um pouco diferente da
original. Enquanto na GCL a ausência de guardas com valor lógico verdadeiro no seu
conjunto de comandos guardados da instrução a tornam equivalente a abort, na
nossa versão essa ausência tem mesmo efeito que a instrução da CxM, Fail. Tiramos
partido disto para tornar as especificações das instruções da máquina mais
compactas: de facto a instrução Fail nunca precisa de ser usada de forma explicita.
Além da alteração anterior introduzimos também algumas adições à GCL: funções
com o comando associado return, e ainda registos de dados e apontadores.
Em relação aos apontadores consideramos que todos eles referem simples palavras
de memória. A operação de desreferenciação representa-se pelo operador unário
prefixo “*” de forma que se V for uma referência, “*V” representa a palavra apontada
por V. Definem-se as operações de soma e subtracção entre um apontador e um inteiro
da seguinte forma: “V+n” representa uma referência para n palavras à frente daquela
que é apontada por V; “V-m” uma referência para m palavras atrás.
Em relação aos registos de dados, a notação “R(F)” indica o campo F do registo de
dados indicado por R.
B.3. Operações básicas da CxM
Apresentam-se aqui as operações básicas da máquina CxM, as quais são usadas
como funções auxiliares pelas instruções da máquina abstracta.
IsLocalVar(v)
return IsVar(v) and v >= H
Testa se v é uma variável local, isto é se ela encontra no stack local.
IsGlobalVar(v)
return IsVar(v) and v < H
Testa se v é uma variável global, isto é, se ela encontra no heap.
IsCurrEnvVar(v)
return IsLocalVar(v) and v < E
Testa se v é uma variável pertencente ao registo de activação do predicado corrente.
IsToTrailVar(v)
return v < HB or v >= B
Testa se a variável v precisa de ser memorizada no trail para futura reposição. As condições
de memorização são as seguintes: ou a variável é global e definida antes da posição do topo do
heap no momento da criação do último ponto de escolha (valor indicado pelo registo HB), ou a
variável é local e definida antes do último ponto de escolha.
B. Operações Básicas e Instruções da CxM
111
ResetVar(v)
*v ← v
Torna a variável v livre.
PushVar(sp)
ResetVar(sp)
sp ← sp + 1
return sp - 1
Empilha uma variável livre na pilha cujo topo é referido por sp.
Drf(v)
do
IsVar(v) and v <> *v → v ← *v
od
return v
Determina o valor da variável v através do percurso completo da sua cadeia de referências.
Trail(v)
if
IsToTrailVar(v) → Push(TR, v)
not IsToTrailVar(v) → skip
fi
Memoriza uma variável no trail caso isso seja necessário.
Assign(v, a)
Trail(v)
*v ← a
Instancia a variável v com o valor a.
Bind(v1, v2)
if
v1 < v2 →
if
v1 > v2 →
fi
if
v1 = v2 →
fi
skip
IsGlobalVar(v1) → Assign(v2, v1)
not IsGlobalVar(v1) → Assign(v1, v2)
IsGlobalVar(v2) → Assign(v1, v2)
not IsGlobalVar(v2) → Assign(v2, v1)
fi
Unifica duas variáveis livres. Esta operação, relativamente à WAM, exige o teste suplementar
IsGlobalVar(.), devido à nova disposição dos stacks.
B. Operações Básicas e Instruções da CxM
112
Unify(t1, t2)
saveH ← H
Push(H, t1)
Push(H, t2)
do H <> saveH →
t1 ← Pop(H)
t2 ← Pop(H)
if
IsVar(t1) →
if
IsVar(t2) → Bind(t1, t2)
not IsVar(t2) → Assign(t1, t2)
fi
not IsVar(t1) →
if
IsVar(t2) → Assign(t2, t1)
not IsVar(t2) →
if
t1 = t2 → skip
IsList(t1) and IsList(t2) →
Push(H, ListTail(t1))
Push(H, ListTail(t2))
Push(H, ListHead(t1))
Push(H, ListHead(t2))
IsStruct(t1) and IsStruct(t2)
and StructFunctor(t1) = StructFunctor(t2) →
dotimes(i, StructArity(t1))
( Push(H, StructArgs(t1)i))
Push(H, StructArgs(t2)i)) )
fi
fi
fi
od
Unifica dois termos quaisquer. O stack global é usado como pilha auxiliar. A CxM não
suporta occurs check por razões de eficiência. No entanto, caso a sua adição venha a ser
desejada, bastará modificar esta operação.
RestoreTrail(b)
do TR <> b →
t ← Pop(TR)
ResetVar(t)
do
Liberta as variáveis que se encontram no trail até ao nível b.
TidyTrail()
v ← B(TR)
do v < TR →
if
IsToTrailVar(*v) → v ← v + 1
not IsToTrailVar(*v) → *v ← Pop(TR)
fi
do
Recupera no trail o espaço correspondente a todas as variáveis que nele se encontram
desnecessariamente. Esta operação é usada apenas pelas instruções de cut.
GTopOfLocalStack(EnvSize)
if
L < E → return L
L >= E → return E - EnvSize
fi
Determina o topo do stack local com base no argumento EnvSize, o qual é suposto indicar o
tamanho corrente do registo de activação que se encontra activo. Como se sabe, o tamanho dos
registos de activação varia dinamicamente devido ao seu trimming.
B. Operações Básicas e Instruções da CxM
113
TopOfLocalStack()
return GTopOfLocalStack(*(CP - 1))
Determina o topo do stack local com base no segundo argumento de uma instrução Call.
Esse argumento encontra-se exactamente na posição anterior à do endereço de retorno do
predicado correntemente activo (indicado pelo registo CP) e indica o tamanho do registo de
activação do predicado que fez a chamada, no ponto em que ela foi efectuada.
FindVisiblePredicate(Functor, Unit)
pred ← GetUnitPredicate(Functor, systemUnit)
if
pred <> nil → return pred
pred = nil → skip
fi
pred ← GetUnitPredicate(Functor, Unit)
if
pred <> nil and PredVisible(pred) → return pred
not (pred <> nil and PredVisible(pred)) → return nil
fi
Para um dado functor, procura uma definição de predicado visível numa unit. A unit que
contêm os predicados de sistema é considerada em primeiro lugar pois todos os predicados
que nela estão definidos são considerados locais a todas as units.
SearchContextBelow(Functor)
do true →
if
C(U) = nilUnit → return failPredicate
C(U) <> nilUnit → C ← C(C)
fi
pred ← FindVisiblePredicate(Functor, C(U))
if
pred <> nil → return pred
pred = nil → skip
fi
od
Procura uma definição de predicado visível para um dado functor no contexto corrente, mas
sem considerar o seu topo. Como se pode observar, a base do contexto corrente é determinada
pela unit [].
SearchContext(Functor)
pred ← FindVisiblePredicate(Functor, C(U))
if
pred = nil → return SearchContextBelow(Functor)
pred <> nil → return pred
fi
Procura uma definição de predicado visível, com base no seu functor, no contexto corrente.
SaveState(arity, previousB)
B(C) ← C
B(CC) ← CC
B(C2) ← C2
B(E) ← E
B(CP) ← CP
B(B) ← previousB
B(B0) ← B0
B(TR) ← TR
B(H) ← H
HB ← B(H)
dotimes(n, arity) B(A)n ← Xn
Guarda todos os registos da CxM que são necessários para repor posteriormente o estado
da máquina. O argumento arity indica o número de registos X a salvaguardar e o argumento
previousB o ponto de escolha anterior àquele que está a ser preenchido.
B. Operações Básicas e Instruções da CxM
114
RestoreState(arity)
C ← B(C)
CC ← B(CC)
C2 ← B(C2)
E ← B(E)
CP ← B(CP)
B0 ← B(B0)
RestoreTrail(B(TR))
H ← B(H)
dotimes(n, arity) Xn ← B(A)n
Repõe um estado da máquina previamente guardado. arity indica o número de registos X a
repor.
DiscardLastChoice()
B ← B(B)
HB ← B(H)
Repõe o último ponto de escolha anterior.
InitCxM()
H ← stacksBegin
TR ← trailBegin
E ← B ← B0 ← stacksEnd
C ← stacksEnd - SizeOf(UnitInstance)
C(S) ← C(C) ← CC ← C
C(U) ← topUnit
C(N) ← 0
C2 ← C - SizeOf(HistoricalRegister)
C2(R2) ← C2(C2) ← C2
C2(C) ← C
L ← C2Base ← C2
Inicia a máquina abstracta. É empilhada uma instância da unit [], que define o contexto vazio
inicial e ainda um registo histórico. Este é permanentemente apontado pelo registo da máquina
C2Base, o qual é utilizado na detecção de tentativas de reposição contextos que não tenham
sido previamente guardados.
B.4. Instruções procedimentais e de controlo
Esta classe de instruções encarrega-se principalmente da gestão da invocação de
predicados, o que inclui a transferência de controlo e a criação de registos de
activação. Incluímos as instruções de tratamento do cut também nesta categoria.
Allocate
save ← E
E ← TopOfLocalStack()
E(E) ← save
E(CP) ← CP
E(CC) ← CC
Esta instrução aparece na zona inicial do código de todas as cláusulas que tenham pelo
menos dois golos no corpo, e ainda nas cláusulas em que o seu único golo é uma das fórmulas
especiais da linguagem CxLP: extensão, salvaguarda, reposição e libertação de contexto. Cria
um novo registo de activação no topo do stack local, memorizando nele os registos E, CP e CC.
Call Pred N
B0 ← B
CP ← P + 2
P ← PredCode(Pred)
A instrução Call termina geralmente o código associado à chamada de um golo. Pred é a
referência do predicado chamado, e N é o número de variáveis no registo de activação neste
ponto da cláusula. O valor guardado no registo B0 é utilizado mais tarde se alguma das
cláusulas do predicado invocado contiver um cut. O registo CP memoriza o endereço de retorno
do predicado chamado. A transferência de controlo é concretizada pela alteração do valor de P.
B. Operações Básicas e Instruções da CxM
115
CallVar
functor ← PrepareCall(X0)
pred ← SearchContext(functor)
if
IsCPred(pred) → P++
Jump(PredCode(pred))
IsPrologPred(pred) →
B0 ← B
CP ← P + 1
P ← PredCode(pred)
fi
Esta instrução implementa o meta-call da linguagem CxLP. O termo que representa o golo a
ser derivado encontra-se no registo X0. A rotina PrepareCall copia os argumentos desse termo
para os registos X0, X1, …, produzindo como valor o seu functor principal. A determinação do
predicado a ser chamado exige uma pesquisa no contexto corrente.
Execute Pred
B0 ← B
P ← PredCode(Pred)
A instrução Execute termina, em geral, o código das cláusulas que têm exactamente um golo
no seu corpo. É semelhante à instrução Call com a diferença que não posiciona o registo CP.
DeallocateAndExecute Pred
CP ← E(CP)
CC ← E(CC)
E ← E(E)
B0 ← B
P ← PredCode(Pred)
Esta instrução termina, em geral, o código das cláusulas que têm mais dos que um golo no
seu corpo. Começa por repor o valor dos registos E, CP, e CC, e passa depois o controlo para o
predicado Pred.
ExecuteVar
functor ← PrepareCall(X0)
pred ← SearchContext(functor)
if
IsCPred(pred) → P ← Address(Proceed)
Jump(PredCode(pred))
IsPrologPred(pred) →
B0 ← B
P ← PredCode(pred)
fi
A instrução ExecuteVar termina o código das cláusula cujo corpo é constituído por apenas
um meta-call. É semelhante à instrução CallVar com a diferença que não posiciona o registo
CP.
DeallocateAndExecuteVar
CP ← E(CP)
CC ← E(CC)
E ← E(E)
functor ← PrepareCall(X0)
pred ← SearchContext(functor)
if
IsCPred(pred) → P ← Address(Proceed)
Jump(PredCode(pred))
IsPrologPred(pred) →
B0 ← B
P ← PredCode(pred)
fi
A instrução DeallocateAndExecuteVar termina o código das cláusulas com mais do que um
golo, e cujo último golo seja um meta-call. Começa por repor o valor dos registos E, CP, e CC,
sendo a parte restante da instrução igual a ExecuteVar.
B. Operações Básicas e Instruções da CxM
116
Proceed
P ← CP
C ← CC
Esta instrução termina o código de todas as cláusulas unitárias. Posiciona os registos P e C
com os valores guardados em CP e CC respectivamente.
DeallocateAndProceed
P ← E(CP)
C ← CC ← E(CC)
E ← E(E)
Esta instrução termina todas as cláusulas não unitárias cujo último golo seja uma das
fórmulas especiais da linguagem CxLP: extensão, salvaguarda, reposição e libertação de
contexto.
NeckCut
if
B < B0 →
B ← B0
HB ← B(H)
L ← Min(B,C,C2)
TidyTrail()
B >= B0 →
skip
fi
Esta instrução é gerada no início do código do corpo das cláusulas cujo primeiro golo seja
um cut. Os pontos de escolha situados acima daquele que é indicado indicado por B0 são
eliminados, e a informação que se torna redundante no trail é eliminada.
GetLevel Yn
Yn ← B0
Esta instrução é gerada no início do código do corpo das cláusulas que tenham um cut
internamente. O valor de B0 é copiado para a variável permanente Yn.
Cut Yn
if
B < Yn →
B ← Yn
HB ← B(H)
L ← Min(B,C,C2)
TidyTrail()
B >= Yn → skip
fi
Esta instrução é gerada no ponto de utilização de um cut que se situe depois do primeiro
golo da cláusula. Os pontos de escolha situados acima daquele que é indicado indicado por Yn
são eliminados, e a informação que fica redundante no trail é eliminada.
Fail
P ← B(P)
Usa-se em todos os pontos do código onde o predicado fail/0 é chamado. O registo P é posto
a apontar para a próxima cláusula alternativa, que se encontra registada no último ponto de
escolha. A instrução apontada por P, e que é executada logo a seguir é uma das quatro que
repõem um estado previamente guardado: RetryMeElse, TrustMe, Retry, Trust.
DiscardAndFail
B ← B(B)
HB ← B(H)
L ← Min(B,C,C2)
P ← B(P)
Elimina o último ponto de escolha e considera a próxima cláusula alternativa.
B. Operações Básicas e Instruções da CxM
117
B.5. Instruções Put
Estas instruções correspondem aos argumentos dos golos do corpo cláusula e
carregam os registos X com os argumentos actuais das chamadas de predicados. A
única instrução deste tipo adicionada na CxM, em relação à WAM, foi a instrução
PutValue Zn, Xi que se destina a ser usada nos casos em que um parâmetro de uma unit
ocorre num argumento de um golo. Além disso as instruções PutStructure e PutList
não colocam aqui a máquina em modo de escrita, pois no corpo das cláusulas todas as
instruções da classe Unify foram substituídas pelas instruções correspondentes da
classe Build.
PutVariable Yn, Xi
ResetVar(Yn)
Xi ← Yn
Esta instrução é gerada para um argumento de um golo que seja uma variável permanente
livre. A variável Yn é tornada livre, e a sua referência copiada para Xi .
PutVariable Xn, Xi
Xi ← Xn ← PushVar(H)
Esta instrução é gerada para um argumento do último golo que seja uma variável livre. É
criada uma variável livre nova no topo do heap, e Xi e Xn são postos a apontar para ela.
PutVariableOne Xn
Xn ← PushVar(H)
Semelhante a PutVariable Xn , Xi usa-se no caso em que i = n.
PutValue ψn, Xi
; ψ ∈ { X, Y, Z }
Xi ← ψn
Esta instrução é gerada para um argumento de um golo que seja uma variável instanciada.
ψn é simplesmente copiado para Xi .
PutUnsafeValue Yn, Xi
t ← Drf(Yn)
if
IsCurrEnvVar(t) →
Xi ← PushVar(H)
Assign(t, Xi)
not IsCurrEnvVar(t) → Xi ← t
fi
Esta instrução é gerada apenas no último golo da cláusula, na primeira referência que aí é
feita a uma variável unsafe. Se a variável depois de desreferenciada apontar para uma variável
situada no registo de activação corrente (prestes a ser eliminado por uso de LCO), ela é
transferida para o topo do heap. Quer isto suceda quer não, o valor com que a variável fica é
copiado para Xi.
PutConstant ϑ, Xi
Xi ← ϑ
Gerada para os argumentos constantes de um golo. A constante é simplesmente copiada
para X i .
PutNil Xi
Xi ← []
Gerada quando um argumento de um golo é o átomo []. Este átomo é simplesmente copiado
para X i .
B. Operações Básicas e Instruções da CxM
118
PutStructure Φ, Xi
Xi ← TagStruct(H)
Push(H, Φ)
Indica o início de uma estrutura que aparece como num argumento de um golo. Xi recebe um
apontador para estrutura com o endereço do topo do heap onde é empilhado o functor.
PutList Xi
Xi ← TagList(H)
Indica o início de uma lista que aparece num argumento de um golo. Xi recebe um apontador
para lista com o endereço do topo do heap, que é o local onde os seus dois argumentos serão a
seguir empilhados.
B.6. Instruções Get
Estas instruções
são
geradas
para
a
cabeça
das
cláusulas
e
fazem
o
emparelhamento dos seus argumentos formais com os argumentos actuais da
chamada dos predicados, que são recebidos nos registos X da máquina. A única
instrução adicionada na CxM, em relação à WAM, foi a instrução GetValue Z n , X i
que se destina a ser usada nos casos em que um parâmetro de uma unit ocorre na
cabeça de uma cláusula ao nível da superfície.
GetVariable Yn, Xi
Yn ← Xi
Esta instrução é gerada para todas as ocorrências de variáveis livres ao nível da superfície
da cabeça da cláusula. Ela copia simplesmente o valor do registo X i para a variável Yn.
GetValue Xn, Xi
if
Unify(Xn, Xi) → Xn ← Drf(Xn)
fi
Esta instrução é gerada para todas as ocorrências de variáveis temporárias instanciadas ao
nível da superfície da cabeça da cláusula. Os dois registos são unificados. O resultado
desreferenciado fica em Xn.
GetValue ψn, Xi
; ψ ∈ { Y, Z }
if
Unify(ψn, Xi) → skip
fi
Esta instrução é gerada para todas as ocorrências de variáveis permanentes instanciadas
ou parâmetros de unit na cabeça da cláusula ao nível da superfície. ψn e Xi são simplesmente
unificados.
GetConstant ϑ, Xi
t ← Drf(Xi)
if
IsVar(t) → Assign(t, ϑ)
t = ϑ → skip
fi
Esta instrução representa a constante ϑ ocorrendo como argumentos da cláusula. Se o
resultado for uma variável então esta é instanciada com ϑ. Caso contrário se o resultado for
diferente de ϑ ocorre retrocesso.
B. Operações Básicas e Instruções da CxM
119
GetNil Xi
t ← Drf(Xi)
if
IsVar(t) → Assign(t, [])
t = [] → skip
fi
Esta instrução representa o átomo [] ocorrendo como argumento da cláusula. Se o resultado
for uma variável então esta é instanciada com []. Caso contrário se o resultado for diferente de []
ocorre retrocesso.
GetStructure Φ, Xi
t ← Drf(Xi)
if
IsVar(t) →
Assign(t, TagStruct(H))
Push(H, Φ)
SetWriteMode()
IsStruct(t) and StructFunctor(t) = Φ →
S ← StructArgs(t)
SetReadMode()
fi
Indica o início de uma estrutura que ocorre na cabeça de uma cláusula. O registo Xi é
desreferenciado. Se o resultado for uma variável, então esta recebe um apontador para o topo
do heap, marcado com a tag estrutura. Além disso é ainda empilhado o functor Φ e posta a
máquina em modo de escrita. Se o resultado for uma estructura com functor Φ, então o registo S
é posto a apontar para o seu primeiro argumento e a máquina é posta em modo de leitura. Nos
casos restante ocorre retrocesso.
GetList Xi
t ← Drf(Xi)
if
IsVar(t) →
IsList(t) →
Assign(t, TagList(H))
SetWriteMode()
S ← ListArgs(t)
SetReadMode()
fi
Indica o início de uma estrutura que ocorre na cabeça de uma cláusula. O registo Xi é
desreferenciado. Se o resultado for uma variável então esta recebe um apontador para o topo do
heap, marcado com a tag lista e a máquina é posta em modo de escrita. Caso o resultado seja
uma lista então o registo S é posto a apontar para o seu primeiro argumento e a máquina é
posta em modo de leitura. Nos casos restante ocorre retrocesso.
B.7. Instruções Unify
Estas instruções, geradas no nosso sistema apenas na cabeça das cláusulas,
correspondem a argumentos de estruturas ou listas e o seu funcionamento depende do
modo corrente: leitura ou escrita. Em modo de leitura fazem-se unificações com
estruturas pré-existentes e em modo de escrita constroem-se novas estruturas. Na
WAM essas instruções também são utilizadas no corpo das cláusulas, mas como nesse
contexto funcionam sempre em modo de escrita, foram substituídas pelas instruções
mais compactas e eficientes da classe Build.
As únicas instruções deste tipo adicionadas na CxM, em relação à WAM, foram as
instruções UnifyValue Zn e UnifyLocalValue Zn que se destinam a ser usada nos casos
em que um parâmetro de uma unit ocorre na cabeça de uma cláusula dentro de uma
estrutura ou lista.
B. Operações Básicas e Instruções da CxM
120
UnifyVoid N
if
WriteMode() → dotimes(i,N) PushVar(H)
ReadMode() → S ← S + N
fi
Representa N variáveis consecutivas de ocorrência única, nos argumentos duma estrutura
da cabeça da cláusula. Consoante o modo corrente seja de escrita ou de leitura, assim são
empilhadas N variáveis livres no topo do heap, ou são saltados os próximos N argumentos
apontados por S.
UnifyVoidOne
if
WriteMode() → PushVar(H)
ReadMode() → S ← S + 1
fi
Especialização da instrução anterior para N = 1.
UnifyVariable ψn
; ψ ∈ { X, Y }
if
WriteMode() → ψn ← PushVar(H)
ReadMode() → ψn ← *S
S ← S + 1
fi
Esta instrução é gerada para argumentos de estruturas da cabeça que sejam variáveis livres.
Em modo de escrita ψn recebe um apontador para uma nova variável livre que é empilhada no
heap. Em modo de leitura o próximo argumento apontado por S é copiado para ψn.
UnifyValue Xn
if
WriteMode() → Push(H, Xn)
ReadMode() → if
Unify(Xn, *S) →
Xn ← Drf(Xn)
S ← S + 1
fi
fi
Esta instrução é gerada para argumentos de estruturas da cabeça que sejam variáveis
temporárias instanciadas com um valor global. Em modo de escrita o valor da variável é
simplesmente empilhado no heap. Em modo de leitura o argumento apontado por S é unificado
com a variável, sendo o resultado desreferenciado deixado na variável.
UnifyValue ψn
; ψ ∈ { Y, Z }
if
WriteMode() → Push(H, ψn)
ReadMode() → if
Unify(ψn, *S) → S ← S + 1
fi
fi
Esta instrução é gerada para argumentos de estruturas da cabeça que sejam variáveis
permanentes ou parâmetros de unit instanciados com um valor global. Em modo de escrita o
valor da variável é simplesmente empilhado no heap. Em modo de leitura o argumento
apontado por S é unificado com a variável.
B. Operações Básicas e Instruções da CxM
121
UnifyLocalValue Xn
if
WriteMode() →
t ← Drf(Xn)
if
IsLocalVar(t) →
Xn ← PushVar(H)
Assign(t, Xn)
not IsLocalVar(t) →
Xn ← t
Push(H, t)
fi
ReadMode() → if
Unify(Xn, *S) →
X ← Drf(Xn)
S ← S + 1
fi
fi
Esta instrução é gerada para argumentos de estruturas da cabeça que sejam variáveis
temporárias instanciadas com um valor não necessariamente global. Em modo de escrita o
valor da variável é empilhado no heap se não for uma referência para uma variável local. Caso
contrário ela é transferida para o topo do heap ficando X n a apontar para ela. Em modo de
leitura a instrução funciona como UnifyValue Xn .
UnifyLocalValue ψn
; ψ ∈ { Y, Z }
if
WriteMode() →
t ← Drf(ψn)
if
IsLocalVar(t) → Assign(t, PushVar(H))
not IsLocalVar(t) → Push(H, t)
fi
ReadMode() → if
Unify(ψn, *S) → S ← S + 1
fi
fi
Esta instrução é gerada para argumentos de estruturas da cabeça que sejam variáveis
permanentes ou parâmetros de unit instanciadas com um valor não necessariamente global.
Em modo de escrita o valor da variável é empilhado no heap se não for uma referência para uma
variável local. Caso contrário ela é transferida para o topo heap. Em modo de leitura a instrução
funciona como UnifyValue ψn .
UnifyConstant ϑ
if
WriteMode() → Push(H, ϑ)
ReadMode() → t ← Drf(*S)
S ← S + 1
if
IsVar(t) → Assign(t, ϑ)
t = ϑ → skip
fi
fi
Representa um argumento de uma estrutura da cabeça que é uma constante. Em modo de
escrita a constante é empilhada no heap. Em modo de leitura a constante é unificada com o
argumento apontado por S.
UnifyNil
if
WriteMode() → Push(H, [])
ReadMode() → t ← Drf(*S)
S ← S + 1
if
IsVar(t) → Assign(t, [])
t = [] → skip
fi
fi
Especialização da instrução anterior para ϑ = [].
B. Operações Básicas e Instruções da CxM
122
B.8. Instruções Build
Estas instruções são geradas apenas no corpo das cláusulas nos argumentos de
estruturas ou listas. Não existem na WAM.
BuildVoid N
dotimes(i,N) PushVar(H)
Representa N variáveis consecutivas de ocorrência única, nos argumentos de um estrutura
ocorrendo num golo da cláusula. São empilhadas N variáveis livres no topo do heap.
BuildVoidOne
PushVar(H)
Especialização da instrução anterior para n = 1.
BuildVariable ψn
; ψ ∈ { X, Y }
ψn ← PushVar(H)
Esta instrução é gerada para argumentos de estruturas que ocorram em golos do corpo que
sejam variáveis livres. ψn recebe um apontador para uma nova variável livre que é empilhada no
heap.
BuildValue ψn
; ψ ∈ { X, Y, Z }
Push(H, ψn)
Esta instrução é gerada para argumentos de estruturas de golos do corpo da cláusula que
sejam variáveis instanciadas com um valor global. O valor da variável é simplesmente
empilhado no heap.
BuildLocalValue Xn
t ← Drf(Xn)
if
IsLocalVar(t) →
Xn ← PushVar(H)
Assign(t, Xn)
not IsLocalVar(t) → Push(H, t)
fi
Esta instrução é gerada para argumentos de estruturas de golos do corpo da cláusula que
sejam variáveis temporárias instanciadas com um valor não necessariamente global. O valor da
variável é empilhado no heap se não for uma referência para uma variável local. Caso contrário
ela é transferida para o topo heap ficando Xn a apontar para ela.
BuildLocalValue ψn
; ψ ∈ { Y, Z }
t ← Drf(ψn)
if
IsLocalVar(t) → Assign(t, PushVar(H))
not IsLocalVar(t) → Push(H, t)
fi
Esta instrução é gerada para argumentos de estruturas de golos do corpo da cláusula, que
sejam variáveis permanentes ou parâmetros de unit instanciadas com um valor não
necessariamente global. O valor da variável é empilhado no heap se não for uma referência
para uma variável local. Caso contrário ela é transferida para o topo heap.
BuildConstant ϑ
Push(H, ϑ)
Representa uma constante como argumento de uma estrutura num golo da cláusula. Ela é
apenas empilhada no heap.
BuildNil
Push(H, [])
Especialização da instrução anterior para ϑ = [].
B. Operações Básicas e Instruções da CxM
123
B.9. Instruções de indexação
As instruções de indexação servem para ligar entre si as diversas cláusulas que
constituem um procedimento, e para efectuar uma pré-selecção das cláusulas a serem
consideradas após a invocação de um predicado, com base no valor do primeiro
argumento. Em relação à WAM surgem aqui diversas alterações: as instruções
SwitchOnConstant e SwitchOnStructure têm mais um argumento que se destina ao
suporte
do
índice
rápido,
foram
adicionadas
novas
instruções,MakeIndex,
SwitchOnList, SwitchOnFunctor e JumpOnVar, e a gestão dos pontos de escolha
recorre à utilização do novo registo L.
MakeIndex Pred
DoIndex(Pred)
P ← PredCode(Pred)
Esta é a instrução de ligação da CxM que se aplica a nomes de predicados definidos
localmente. Tal como as outras instruções de ligação, ela nunca é gerada pelo compilador mas
sim pelo módulo gestor da base de dados do sistema, que a coloca no campo Inst do descritor
do predicado nas circunstâncias descritas em 5.3. A instrução começa por chamar um módulo
do compilador que trata da construir o índice se valer a pena, e estabelece a ligação para o
predicado mediante a alteração do valor de PredCode(Pred). O controlo é então passado para o
código do predicado.
TryMeElse Next Arity
save ← B
L ← B ← TopOfLocalStack() - Arity - SizeOf(ChoicePoint)
B(P) ← Next
SaveState(Arity, save)
Esta instrução precede o código da primeira cláusula de um predicado composto por várias.
Ela cria um ponto de escolha e guarda nele o estado corrente da máquina. Arity representa a
aridade do predicado constituindo o número de registos X a salvaguardar no ponto de escolha.
Next é o endereço da próxima cláusula a ser considerada quando houver retrocesso.
RetryMeElse Next Arity
B(P) ← Next
RestoreState(Arity)
L ← B
Esta instrução precede o código de uma cláusula intermédia de um predicado composto por
várias. Ela repõe o estado guardado no último ponto de escolha e guarda nele o endereço da
próxima cláusula a considerada quando houver retrocesso.
TrustMe Arity
L ← B + Arity + SizeOf(ChoicePoint)
RestoreState(Arity)
B ← B(B)
HB ← B(H)
Esta instrução precede o código da última cláusula de um predicado composto por várias.
Ela repõe o estado guardado no último ponto de escolha eliminando-o seguidamente.
Try Label
arity ← *(Label - 1)
save ← B
L ← B ← TopOfLocalStack() - arity - SizeOf(ChoicePoint)
B(P) ← P + 1
SaveState(arity, save)
P ← Label
A instrução Try indica a primeira cláusula de uma sequência de cláusulas com a mesma
chave. Ela cria um ponto de escolha e guarda nele o estado corrente da máquina. Label é o
B. Operações Básicas e Instruções da CxM
124
endereço do início do código da cláusula a executar. Em caso de retrocesso será executada a
instrução Retry ou Trust que se encontra a seguir. A aridade do predicado é obtida acedendo ao
segundo argumento da instrução de indexação que se encontra no início da cláusula referida
por Label.
Retry Label
arity ← *(Label - 1)
B(P) ← P + 1
RestoreState(arity)
L ← B
P ← Label
A instrução Retry indica uma cláusula intermédia de uma sequência de cláusulas com a
mesma chave. Ela repõe o estado salvaguardado no último ponto de escolha e posiciona nele o
endereço da instrução seguinte, a ser considerado quando houver retrocesso.
Trust Label
arity ← *(Label - 1)
L ← B + arity + SizeOf(ChoicePoint)
RestoreState(arity)
B ← B(B)
HB ← B(H)
P ← Label
A instrução Trust indica a última cláusula de uma sequência de cláusulas com a mesma
chave. Ela repõe o estado guardado no último ponto de escolha eliminando-o seguidamente. O
controlo é então passado para o código da última cláusula do predicado.
SwitchOnTerm V C L S
t ← Drf(X0)
if
IsVar(t) → P ← V
IsAtomic(t) → P ← C
IsList(t) → P ← L
IsStruct(t) → P ← S
fi
Esta instrução inicia sempre o índice rápido e também algumas sub-sequências
constituintes do índice compacto (cf. 7.7). Ela passa o controlo para um de quatro diferentes
endereços, consoante o tipo do valor de X0 depois de desreferenciado. V, C, L, e S são os
endereços associados respectivamente a variável, constante, lista e estrutura.
SwitchOnList V C L S
t ← Drf(X0)
if
IsVar(t) → P ← V
IsAtomic(t) → if t = [] → P ← C fi
IsList(t) → P ← L
S ← ListArgs(t)
SetReadMode()
IsStruct(t) → P ← S
fi
Esta é uma especialização da instrução SwitchOnTerm. Aplica-se a um predicado que entre
as suas diversas cláusulas tenha apenas uma cujo primeiro argumento seja o átomo [], e outra
cujo primeiro argumento seja uma lista. A instrução executa logo no seu corpo as instruções
GetList X0 e GetNil X0, e salta por cima dessas instruções no código das cláusulas.
B. Operações Básicas e Instruções da CxM
125
SwitchOnFunctor V C L S
t ← Drf(X0)
if
IsVar(t) → P ← V
IsAtomic(t) → P ← C
IsList(t) → P ← L
IsStruct(t) →
if StructFunctor(t) = *(S-1) →
P ← S
S ← StructArgs(t)
SetReadMode()
fi
fi
Esta é uma especialização da instrução SwitchOnTerm. Aplica-se a um predicado que entre
as suas diversas cláusulas tenha apenas uma cujo primeiro argumento seja uma estrutura. A
instrução executa logo no seu corpo a instrução GetStruct Φ, X0, e saltando por cima dela no
código da cláusula.
JumpOnVar Label
t ← Drf(X0)
if
IsVar(t) → P ← V
not IsVar(t) → skip
fi
Esta instrução surge sempre, e apenas, no início do índice compacto (cf. 7.7.3). Ela passa o
controlo para o endereço Label se o valor de X0 depois de desreferenciado for uma variável.
SwitchOnConstant N Table Else
t ← Drf(X0)
res ← HashVal(t,N,Table)
if
res <> nil → P ← res
res = nil → P ← Else
fi
Esta instrução é gerada para controlar o acesso a um grupo de cláusulas cujos primeiros
argumentos sejam constantes. Table é o endereço de uma tabela de dispersão na qual, para
cada uma dessas constantes, está registado o endereço do código a executar. Se o valor de X0
não estiver previsto na tabela, o controlo passa para o endereço Else. N é uma potência de 2 que
indica o tamanho da tabela de dispersão.
SwitchOnStructure N Table Else
t ← Drf(X0)
t ← StructFunctor(t)
res ← HashVal(t,N,Table)
if
res <> nil → P ← res
res = nil → P ← Else
fi
Esta instrução é parecida com a anterior e aplica-se a grupos de cláusulas cujos primeiros
argumentos sejam estruturas. A selecção é efectuada na tabela de dispersão Table com base no
functor principal do primeiro argumento da chamada.
B.10. Instruções para fórmulas de extensão
Estas são todas as instruções específicas para tratamento das fórmulas de
extensão. Existem instruções para estender e contrair o contexto corrente, e
instruções para efectuar chamadas de nomes que ocorrem em fórmulas de extensão.
B. Operações Básicas e Instruções da CxM
126
PushContext Unit FirstReg EnvSize
n ← UnitNEnvCells(Unit)
L ← C ← GTopOfLocalStack(EnvSize) - n - SizeOf(UnitInstance)
C(U) ← Unit
C(C) ← CC
C(N) ← n
dotimes(i, n) C(Cache)3*i ← 0
dotimes(i, UnitArity(Unit)) Zi ← Xi+FirstReg
CC ← C
Esta instrução é gerada para as fórmulas de extensão U>>G nos casos em que U é um
designador de unit. A instrução precede o código do golo G. Ela empilha no stack local uma nova
instância de unit e preenche os seus diversos campos. Unit é uma referência para a unit que
estende o contexto, FirstReg o índice do registo X a partir do qual vêm os argumentos do
designador de unit e EnvSize é o tamanho do registo de activação corrente no ponto em que
surge a fórmula. Os parâmetros da unit (variáveis Z) são inicializados a partir dos registos X, e
as posições da cache das chamadas dependentes do contexto correspondentes ao número de
versão são inicializadas com um valor impossível, neste caso zero, para forçar o
estabelecimento de ligações na fase inicial de utilização da cache. No final CC recebe C: sendo
a lógica da utilização do registo CC semelhante à da utilização de CP aquela atribuição poderia
ser feita em todas as chamadas de predicado; no entanto para não prejudicar os programas que
não usem contextos, a atribuição é feita nas instruções de expansão e contracção do contexto
sempre na espectativa que uma dessas chamadas seja feita.
PushVarContext UseReg EnvSize
t ← Drf(XUseReg)
Unit ← TermToUnit(t)
if
Unit <> nil → skip
fi
n ← UnitNEnvCells(Unit)
L ← C ← GTopOfLocalStack(EnvSize) - n - SizeOf(UnitInstance)
C(U) ← Unit
C(C) ← CC
C(N) ← n
dotimes(i, n) C(Cache)3*i ← 0
dotimes(i, UnitArity(Unit)) Zi ← Drf(StructArgs(t)i)
CC ← C
Esta instrução é gerada para as fórmulas de extensão U>>G nos casos em que U é uma
variável. Ela precede o código do golo G. Empilha no stack local uma nova instância de unit e
preenche os seus campos. O argumento UseReg é o índice do registo X onde se encontra o
designador da unit que estende o contexto e EnvSize e o tamanho do registo de activação
corrente no ponto em que surge a fórmula. Os parâmetros da unit (variáveis Z) são inicializados
a partir dos argumentos do designador de unit que se encontra no registo X UseReg. A forma da
inicialização da cache e a razão da transferência de C para CC são explicados na instrução
anterior.
PopContext
if
L = C → L ← C + C(N) + SizeOf(UnitInstance)
L <> C → skip
fi
C ← CC ← C(C)
Esta instrução é gerada em todas as fórmulas de extensão U>>G aparecendo imediatamente
a seguir ao código do golo G. Em primeiro lugar ela remove a instância de unit que se encontra
no topo do contexto corrente, caso o golo G tenha sido resolvido deterministicamente. Isto pode
ser detectado através de uma comparação de L com C pois aquela condição verifica-se se e só
se a instância da unit estiver no topo do stack local. Seguidamente os valores de C e CC são
repostos.
B. Operações Básicas e Instruções da CxM
127
MakeCtxExtBinding Pred
DoCtxExtBind(Pred)
P ← PredCode(Pred)
Esta é a instrução de ligação da CxM que se aplica a nomes de predicado que surgem no
interior das fórmulas de extensão de contexto. Tal como as outras instruções de ligação, ela é
gerada pelo módulo gestor da base de dados do sistema, que a coloca no campo Inst do
descritor do predicado nas circunstâncias descritas em 5.3. A instrução começa por chamar um
módulo do compilador que estabelece a ligação para o predicado mediante a alteração do valor
de PredCode(Pred). O controlo é então passado para esse endereço.
CtxExtJump ExternPred
P ← PredCode(ExternPred)
Esta instrução encontra-se nos descritores de nomes de predicado que ocorrem em
fórmulas de extensão, nos casos em que foi possível estabelecer uma ligação estática em
virtude do facto desse nome ser visível na unit que estendeu o contexto. Ela tem como
argumento o descritor de nome de predicado externo chamado, e executa um simples salto
inter-units, passando o controlo para o código desse predicado.
CtxExtJumpBelow Functor
P ← PredCode(SearchContextBelow(Functor))
Esta instrução encontra-se nos descritores de nomes de predicados que ocorrem em
fórmulas de extensão, nos casos em que esse nome não é visível na unit que estendeu o
contexto. Ela tem como argumento o functor indicador do nome de predicado externo chamado,
e efectua uma busca no contexto corrente de uma definição visível aplicável. O topo do contexto
é ignorado na busca visto que na altura do estabelecimento da ligação (feita pela instrução
MakeCtxExtBinding) ter sido determinada a inexistência de definição adequada nele.
VarCtxExtCall Functor UCache
if
C(U) = UCache(Unit) → pred ← UCache(Pred)
C(U) <> UCache(Unit) → pred ← FindVisiblePred(C(U), Functor)
UCache(Unit) ← C(U)
UCache(Pred) ← pred
fi
B0 ← B
CP ← P + 1
if
pred <> nil → P ← PredCode(pred)
pred = nil → P ← PredCode(SearchContextBelow(Functor)))
fi
É gerada para fórmulas da forma U>>g em que U é variável, para fazer a chamada do nome
associado ao golo g. Esta instrução usa uma pequena cache de duas palavras que permite
detectar se o valor de U se mantém desde a última chamada. Se assim for usa-se a definição de
predicado que se encontra nessa cache, caso contrário procura-se na unit corrente a definição
de predicado, que é de imediato memorizada na cache. Finalmente efectua-se a chamada do
predicado assim obtido. Note-se que, apesar de ser improvável, pode acontecer que o nome do
golo g não esteja visível em U: nesse caso é sempre necessário efectuar uma busca no contexto
corrente, não podendo neste caso a definição de predicado obtida ficar memorizada na cache
por razões de coerência desta.
B.11. Instruções de importação
Estas são as instruções de tratamento de chamadas de nomes importados. Existem
instruções específicas para manipular o contexto corrente e instruções para efectuar
chamadas de nomes importados.
B. Operações Básicas e Instruções da CxM
128
SwitchContextOn Unit FirstReg
n ← UnitNEnvCells(Unit)
L ← C ← TopOfLocalStack() - n - SizeOf(UnitInstance)
C(U) ← Unit
C(S) ← CC
C(C) ← CC(C)
C(N) ← n
dotimes(i, n) C(Cache)3*i ← 0
dotimes(i, UnitArity(Unit)) Zi ← Xi+FirstReg
CC ← C
Esta instrução é gerada pelo compilador, quando produz o segmento de código de ligação
que faz a chamada do nome de predicado de um golo g, importado de um designador de unit U.
Esta instrução aparece sempre antes da instrução de chamada de g. É muito semelhante à
instrução PushContext pois também empilha uma instância de unit no stack local e inicia-a. A
única diferença importante reside no facto de esta operação não fazer uma extensão de
contexto, mas sim uma substituição da instância de unit do topo do contexto. Por isso o campo
C da nova instância de unit toma o mesmo valor que o campo C do anterior topo do contexto. O
topo do contexto anterior é por sua vez guardado no campo S, para posterior reposição.
SwitchVarContextOn UseReg
t ← Drf(XUseReg)
Unit ← TermToUnit(t)
if
Unit <> nil → skip
fi
n ← UnitNEnvCells(Unit)
L ← C ← TopOfLocalStack() - n - SizeOf(UnitInstance)
C(U) ← Unit
C(S) ← CC
C(C) ← CC(C)
C(N) ← n
dotimes(i, n) C(Cache)3*i ← 0
dotimes(i, UnitArity(Unit)) Zi ← Drf(StructArg(t, i))
CC ← C
Esta instrução é gerada pelo compilador quando produz o segmento de código de ligação que
faz a chamada do nome de predicado de um golo g importado de um parâmetro de unit U. A
instrução aparece sempre antes da instrução de chamada de g. É muito semelhante à instrução
PushVarContext com apenas a diferença de não fazer uma extensão de contexto, mas sim uma
substituição lógica da instância de unit que se encontra topo do contexto.
SwitchContextOff
if
L = C → L ← C + C(N) + SizeOf(UnitInstance)
L <> C → skip
fi
C ← CC ← C(S)
Esta instrução aparece no segmento de código de ligação que faz a chamada do nome de
predicado de um golo g, importado de um parâmetro de unit U. A instrução aparece depois da
instrução de chamada de g. Em primeiro lugar ela remove a instância de unit que se encontra no
topo do contexto corrente, caso o golo g tenha sido resolvido deterministicamente.
Seguidamente os valores de C e CC são repostos usando o campo S dessa instância de unit.
MakeImportBinding Pred
DoImport(Pred)
P ← PredCode(Pred)
Esta é a instrução de ligação da CxM que se aplica a nomes de predicado importados. É
gerada pelo módulo gestor da base de dados do sistema, que a instala no descritor do predicado
como é explicado em 5.3. A instrução começa por chamar um módulo do compilador que gera
um segmento de código onde o predicado importado é chamado no contexto modificado. O
controlo passa seguidamente para esse código. Existe mais informação sobre esta instrução no
ponto 5.3.2.2.
B. Operações Básicas e Instruções da CxM
129
ImportJumpBelow Functor
P ← PredCode(SearchContextBelow(Functor))
Esta instrução encontra-se nos descritores de nomes de predicados importados, nos casos
em que esse nome não é visível no designador de unit de onde a importação é feita. Ela tem
como argumento o functor indicador do nome de predicado chamado, e efectua uma busca no
contexto corrente de uma definição visível aplicável. O topo do contexto é ignorado na busca
visto que na altura do estabelecimento da ligação (feita pela instrução MakeImportBinding), ter
sido determinada a inexistência de definição adequada nele (de facto o topo do contexto nem
chega a ser mudado antes da execução desta instrução, por tal se revelar desnecessário).
VarImportCall Functor UCache
if
C(U) = UCache(Unit) → pred ← UCache(Pred)
C(U) <> UCache(Unit) → pred ← FindVisiblePred(C(U), Functor)
UCache(Unit) ← C(U)
UCache(Pred) ← pred
fi
B0 ← B
CP ← P + 1
if
pred <> nil → P ← PredCode(pred)
pred = nil → P ← PredCode(SearchContextBelow(Functor)))
fi
Esta instrução é gerada nas chamadas de predicados importados de parâmetros de unit. Ela
usa uma pequena cache de duas palavras que permite detectar se o valor de U se mantém desde
a última chamada. A descrição que aqui se apresenta é rigorosamente igual à da instrução
VarCtxExtCall. As diferenças entre as duas situam-se a um nível de que aqui não está a ser
considerado, que é o da sua relação com o ambiente de programação, neste caso as facilidades
de depuração (ver descrição do predicado status/2).
B.12. Instruções para nomes dependentes do contexto
Existem apenas as duas seguintes instruções relacionadas com as chamadas de
nomes dependentes do contexto.
MakeCtxDepBinding Pred
DoCtxDepBind(Pred)
P ← PredCode(Pred)
Esta é a instrução de ligação da CxM que se aplica a nomes de predicado dependentes do
contexto. É gerada pelo módulo gestor da base de dados do sistema, que a instala no descritor
do nome de predicado. De acordo com o que foi dito no ponto 5.3.2.4 esta instrução associa um
deslocamento na cache ao nome dependente do contexto e instala a instrução CtxDepJump,
que tem por argumento esse deslocamento, no descritor de predicado. O controlo passa então
para essa instrução.
B. Operações Básicas e Instruções da CxM
130
CtxDepJump Functor CacheOffset
if
CacheOffset < C(N) →
if
C(Cache)CacheOffset = Version(Functor) →
P ← PredCode(C(Cache)CacheOffset+1)
C ← C(Cache)CacheOffset+2
C(Cache)CacheOffset <> Version(Functor) →
CC(Cache)CacheOffset ← Version(Functor)
CC(Cache)CacheOffset+1 ←
SearchContextBelow(Functor))
CC(Cache)CacheOffset+2 ← C
P ← PredCode(C(Cache)CacheOffset+1)
fi
CacheOffset >= C(N) → P ← PredCode(SearchContextBelow(Functor))
fi
Esta instrução aplica-se à chamada de nomes dependentes do contexto. Não é gerada pelo
compilador mas sim pela instrução MakeCtxDepBinding. Ela testa primeiro o argumento
CacheOffset verificando se ele é abrangido pela cache da instância da unit corrente. Em caso
negativo tem de ser feita dinamicamente uma procura no contexto corrente de uma definição de
predicado visível compatível com o argumento Functor. Em caso afirmativo compara-se a
versão corrente do functor com aquela que se encontra memorizada na cache. Se os valores
forem iguais isto significa que a informação da cache para aquele nome está actualizada
podendo ser usada imediatamente. Se os valores forem diferentes a informação da cache para
esse nome tem de ser actualizada o que envolve uma pesquisa no contexto corrente.
B.13. Instruções de gestão do contexto histórico
Estas instruções são usadas no controlo do contexto histórico.
PushHistoricalContext EnvSize
save ← C2
L ← C2 ← GTopOfLocalStack(EnvSize)) - SizeOf(HistoricalRegister)
C2(C) ← C
C2(C2) ← save
C2(R2) ← C2
Esta instrução é gerada para as fórmulas de salvaguarda de contexto >G, aparecendo
sempre antes do código do golo G. Ela empilha no stack local um novo registo histórico e
preenche os seus campos. Guarda em particular o contexto corrente no campo C e uma
auto-referência no campo R2. Recordemos que no esquema de representação dos contextos
históricos (ver 4.4.7.1) o campo R2 do registo histórico que se encontra no topo, indica o topo do
registo histórico conceptual. Assim fica justificado o uso da auto-referência pois neste caso os
topos dos contextos históricos, implementacional e lógico, coincidem. O argumento EnvSize é o
tamanho do registo de activação corrente, no ponto em que surge a fórmula.
PopHistoricalContext
if
L = C2 → L ← C2 + SizeOf(HistoricalRegister)
L <> C2 → skip
fi
C2 ← C2(C2)
Esta instrução é gerada para as fórmulas de salvaguarda de contexto >G, aparecendo
sempre logo depois do código do golo G. O registo histórico do topo é desempilhado, sendo
recuperado o seu espaço, caso o golo G tenha sido resolvido deterministicamente. Esta
condição pode ser detectado através de uma simples comparação de L com C2.
B. Operações Básicas e Instruções da CxM
131
EnterHistoricalContext EnvSize
conceptualTop ← C2(R2)
if
conceptualTop = C2Base → Error("No saved context")
conceptualTop <> C2Base → skip
fi
save ← C2
L ← C2 ← GTopOfLocalStack(EnvSize)) - SizeOf(HistoricalRegister)
C2(C) ← C
C2(C2) ← save
C2(R2) ← conceptualTop(C2)(R2)
C ← CC ← conceptualTop(C)
Esta instrução é gerada para as fórmulas de reposição de contexto <G, aparecendo sempre
antes do código do golo G. Ela repõe o contexto situado no topo do contexto histórico
conceptual e, além disso, empilha no stack local um novo registo histórico onde memoriza o
contexto corrente (pois a reposição de contexto é temporária) e o novo topo do contexto
histórico conceptual, que se reduz (apesar do implementacional aumentar).
ExitHistoricalContext
if
L = C2 → L ← C2 + SizeOf(HistoricalRegister)
L <> C2 → skip
fi
C ← CC ← C2(C)
C2 ← C2(C2)
Esta instrução é gerada para as fórmulas de reposição de contexto <G, aparecendo sempre
depois do código do golo G. O registo histórico do topo é desempilhado, sendo recuperado o
espaço ocupado por ele, caso o golo G tenha sido resolvido deterministicamente. Além disso o
estado dos registos C e CC anterior à reposição do contexto é restaurado.
132
C
Exemplos de código gerado
Exibimos aqui alguns exemplos de codificação de cláusulas na CxM, muitos dos
quais são extraídos de [Warren 83], o que facilita o estabelecimento de uma
comparação com o código gerado para a WAM. Todas as listagens de código foram
obtidas, usando o disassembler incorporado no sistema, que é invocado através do
predicado code/2. O código foi compilado com o indicador do sistema specialize
desligado (cf. predicado status/2 no apêndice A), a fim de o código ficar mais legível.
qsort([],R,R).
qsort([X|L],R0,R) :split(L,X,L1,L2), qsort(L1,R0,[X|R1]), qsort(L2,R1,R).
f5c44 SwitchOnList f4bf0 f4c04 f4c88 FAIL f4cc0
f5c58 Proceed
f4ccc
f4cd8
f4bf0 TryMeElse f4c74 3
f4ce4
f4bfc GetNil X0
f4cf0
f4c04 GetXValue X1 X2
f4cfc
f4c10 Proceed
f4d04
f4d0c
f4c74 TrustMe 3
f4d14
f4c80 GetList X0
f4d20
f4c88 Allocate
f4d2c
f4c8c UnifyYVariable Y5
f4d38
f4c94 UnifyXVariable X0
f4d44
f4c9c GetYVariable Y4 X1
f4ca8 GetYVariable Y2 X2
f4cb4 PutYValue Y5 X1
PutYVariable Y3 X2
PutYVariable Y0 X3
Call split/4 6
PutYValue Y3 X0
PutYValue Y4 X1
PutList X2
BuildYValue Y5
BuildYVariable Y1
Call qsort/3 4
PutUnsafeValue Y0 X0
PutYValue Y1 X1
PutYValue Y2 X2
DeallocateAndExecute qsort/3
C. Exemplos de código gerado
compile(Clause, Instructions) :preprocess(Clause, C1),
translate(C1, Symbols),
number_variables(Symbols, 0, N, Saga),
complete_saga(0, N, Saga),
allocate_registers(Saga),
generate(Symbols, Instructions).
f4f70
f4f74
f4f80
f4f8c
f4f98
f4fa4
f4fb0
f4fbc
f4fc8
f4fd4
f4fe0
Allocate
GetYVariable Y1 X1
PutYVariable Y4 X1
Call preprocess/2 5
PutYValue Y4 X0
PutYVariable Y0 X1
Call translate/2 5
PutYValue Y0 X0
PutConstant 0 X1
PutYVariable Y3 X2
PutYVariable Y2 X3
f4fec
f4ff8
f5004
f5010
f501c
f5028
f5034
f5040
f504c
f5058
Call number_variables/4 4
PutConstant 0 X0
PutYValue Y3 X1
PutYValue Y2 X2
Call complete_saga/3 4
PutYValue Y2 X0
Call allocate_registers/1 3
PutUnsafeValue Y0 X0
PutYValue Y1 X1
DeallocateAndExecute generate/2
d(U*V, X, (DU*V)+(U*DV)) :- d(U,X,DU), d(V,X,DV).
f5178
f5184
f518c
f5190
f5198
f51a4
f51b0
f51b8
f51c0
f51cc
GetStructure */2 X0
UnifyXVariable X0
Allocate
UnifyYVariable Y2
GetYVariable Y1 X1
GetStructure +/2 X2
UnifyXVariable X3
UnifyXVariable X4
GetStructure */2 X3
UnifyXVariable X2
f51d4
f51dc
f51e8
f51f0
f51f8
f5204
f5210
f521c
f5228
f5234
UnifyYValue Y2
GetStructure */2 X4
UnifyXValue X0
UnifyYVariable Y0
PutYValue Y1 X1
Call d/3 3
PutYValue Y2 X0
PutYValue Y1 X1
PutYValue Y0 X2
DeallocateAndExecute d/3
test :- do(parse(s(np,vp),[birds,fly],[])).
f5378
f5380
f5388
f538c
f5394
f539c
f53a4
PutList X1
BuildConstant 'fly'
BuildNil
PutList X2
BuildConstant 'birds'
BuildXValue X1
PutStructure s/2 X1
f53b0
f53b8
f53c0
f53cc
f53d4
f53dc
f53e0
BuildConstant 'np'
BuildConstant 'vp'
PutStructure parse/3 X0
BuildXValue X1
BuildXValue X2
BuildNil
Execute do/1
f5b74
f5b7c
f5b84
f5b8c
f5b94
f5b9c
f5ba4
f5bac
f5bb4
Try f56d8
Retry f5758
Retry f5804
Trust f5840
Try f5618
Retry f56d8
Trust f5758
Try f5664
Retry f56d8
q(X + Y) :- q(X).
q(X + Y) :- q(Y).
q(trace) :- trace.
q(notrace) :- notrace.
q(nl) :- nl.
q(X) :- sys(X).
q(X) :- ext(X).
q(q(X)) :- q(X).
q(repeat).
q(repeat) :- q(repeat).
q(true).
f5b40 SwitchOnTerm f5478 f5b64 f5b54 f5bf4
f5b54 Try f56d8
f5b5c Trust f5758
f5b64 SwitchOnConstant f5ac0 8 f5b54
nl f5bac
repeat f5b74
trace f5bc4
notrace f5b94
true f5bdc
133
C. Exemplos de código gerado
f5bbc
f5bc4
f5bcc
f5bd4
f5bdc
f5be4
f5bec
f5bf4
f5c04
f5c0c
f5c14
f5c1c
f5c24
f5c2c
f5c34
f5c3c
Trust f5758
Try f5594
Retry f56d8
Trust f5758
Try f56d8
Retry f5758
Trust f58a0
SwitchOnStructure f5b24 2 f5b54
+/2 f5c04
q/1 f5c24
Try f5484
Retry f54f0
Retry f56d8
Trust f5758
Try f56d8
Retry f5758
Trust f57a0
Proceed
f5478
f5484
f5490
f5498
f549c
TryMeElse f54e4 1
GetStructure +/2 X0
UnifyXVariable X0
UnifyVoidOne
Execute q/1
f54e4
f54f0
f54fc
f5500
f5508
RetryMeElse f5588 1
GetStructure +/2 X0
UnifyVoidOne
UnifyXVariable X0
Execute q/1
f5588 RetryMeElse f560c 1
f5594 GetConstant 'trace' X0
f55a0 Execute trace/0
f560c RetryMeElse f5658 1
f5618 GetConstant 'notrace' X0
f5624 Execute notrace/0
f5658
f5664
f5670
f5674
RetryMeElse f56cc 1
GetConstant 'nl' X0
nl/0
Proceed
f56cc RetryMeElse f574c 1
f56d8 Execute sys/1
f574c RetryMeElse f5794 1
f5758 Execute ext/1
f5794
f57a0
f57ac
f57b4
RetryMeElse f57f8 1
GetStructure q/1 X0
UnifyXVariable X0
Execute q/1
f57f8 RetryMeElse f5834 1
f5804 GetConstant 'repeat' X0
f5810 Proceed
f5834
f5840
f584c
f5858
RetryMeElse f5894 1
GetConstant 'repeat' X0
PutConstant 'repeat' X0
Execute q/1
f5894 TrustMe 1
f58a0 GetConstant 'true' X0
f58ac Proceed
a(X,Y) :- Y >>b(X).
a(X,Y) :- X >>b(Y).
a(X,Y) :- hello(x,Y) >> (>b(X)).
f5920
f592c
f5930
f593c
f5944
f5948
TryMeElse f5990 2
Allocate
PushVarContext X1 0
VarCtxExtCall b/1
PopContext
DeallocateAndProceed
f5990
f599c
f59a0
f59ac
f59b8
f59c0
RetryMeElse f5a44 2
Allocate
PushVarContext X0 0
PutXValue X1 X0
VarCtxExtCall b/1
PopContext
f59c4 DeallocateAndProceed
f5a44
f5a50
f5a54
f5a60
f5a6c
f5a7c
f5a84
f5a90
f5a94
f5a98
TrustMe 2
Allocate
PutConstant 'x' X2
PutXValue X1 X3
PushContext hello 2 0
PushHistoricalContext 0
Call b/1@hello 0
PopHistoricalContext
PopContext
DeallocateAndProceed
unit authors(Work).
import wrote/2 from Work.
author(X) :- wrote(X,_).
wrote/2:
f4fd0 Allocate
f4fd4 PutZValue Z0 X2
f4fe0 SwitchVarContextOn X2
f4fe8 VarImportCall wrote/2
f4ff0 SwitchContextOff
f4ff4 DeallocateAndProceed
author/1:
f4d5c TryMeElse f4e58 1
f4d68 PutVariableOne X1
f4d70 Execute wrote/2
134
135
Bibliografia
[Aït-Kaci 90]
Aït-Kaci, H.: “The WAM: A (Real) Tutorial”, Digital, Paris Research Laboratory,
Janeiro 1990
[Aho 86]
Aho, A.; Sethi, R.; Ullman, J.: “Compilers, Principles, Techniques and Tools”
Addison Wesley 1986
[Appleby 88]
Appleby, K.; Carlsson, M.; Haridi, S.; Sahlin D.: “Garbage Collection for Prolog
based on WAM” em Communications of the ACM, Vol. 31, Nº 6, Junho 1988
[Bacha 87]
Bacha, H: “Meta-level Programming: a Compiled Approach”, em Proc. 4º ICLP,
Melbourne, Australia, MIT Press, 1987
[Bacha 88]
Bacha, H: “Meta-Prolog Design and Implementation”, em Proc. 5º ICLP, USA, MIT
Press, 1988
[Battani 73]
Battani, G.; Meloni, H.: “Interpréteur du language de programmation Prolog””,
Groupe d’Intelligence Artificielle, Université d’Aix-Marseille, 1973
[Bell 73]
Bell, J.: “Threaded code”, em Communications of the ACM, Vol. 16, 370-372, 1973
[Bendl 80]
Bendl, J.; Köves, P; Szeredi P.: “The MProlog System”, em Tärnlund 1980, pp.201209
Bibliografia
136
[Bowen 83]
Bowen, L.; Byrd D.; Clocksin W.: “A Portable Prolog Compiler”, em Logic
Programming Workshop ‘83
[Bowen 86]
Bowen, K.; Buettner K.; Cicekli I.; Turk A.: “The Design and Implementation of a
High-Speed Incremental Portable Prolog Compiler”, em Proc. 3º ICLP, London,
Springer-Verlag, 1986
[Bruynooghe 76]
Bruynooghe, M: “An Interpreter for Predicate Logic Programs: Part I”, Report CW
10, Applied Mathematics and Programming Division, Katholieke Universiteit,
Leuven, Belgica 1976
[Bruynooghe 82]
Bruynooghe, M: “The Memory Management of Prolog Implementations”, em Logic
Programming, Academic Press, 1982
[Buettner 86]
Buettner, K.: “Fast Decompilation of Compiler Prolog Clauses”, em Proc. 3º ICLP,
London, Springer-Verlag, 1986
[Caneghem 86]
Caneghem, M.: “L’Anatomie de Prolog”, Inter Editions, Paris, 1986
[Clocksin 81]
Clocksin, W; Mellish, C.: “Programming in Prolog”, Springer-Verlag, 1981
[Clocksin 85]
Clocksin, W: “Implementation Techniques for Prolog Data Bases”, em Software Practice and Experience, vol.15, 1985
[Colmerauer 72]
Colmerauer, A.; Kanoui, H.; Pasero, R.; Roussel, P.: “Un système de communication
homme-machine en français”, Groupe d’Intelligence Artificielle, Université
d’Aix-Marseille, Outubro de 1972
[Colmerauer 75]
Colmerauer, A.: “Les grammaires de métamorphose, ”, Groupe d’Intelligence
Artificielle, Université d’Aix-Marseille, Novembro de 1975
[Colmerauer 85]
Colmerauer, A.: “Prolog in 10 Figures”, em Communications of the ACM, Vol. 28,
1296-1310, 1985
[Cousot 77]
Cousot, P.; Cousot, R.: “Abstract Interpretation: A Unified Lattice Model for Static
Analisys of Programs by Construction or Approximation of Fixpoints”, em Conf.
Rec. 4th ACM Symp. on Princ. of Programming Languages, 1977
[Debray 86a]
Debray, S.: “Register Allocation in a Prolog Machine”, em Proc. Symp. on Logic
Programming, Salt Lake City, USA, 1986
[Debray 86b]
Debray, S.; Warren D.: “Automatic Mode Inference for Prolog Programs”, em Proc.
Symp. on Logic Programming, Salt Lake City, USA, 1986
Bibliografia
137
[Debray 86c]
Debray, S.; Warren D.: “Detection and Optimization of Functional Computations
in Prolog”, em Proc. 3º ICLP, Londres, 1986
[Dewar 75]
Dewar, R.: “Indirect Threaded Code”, em Communications of the ACM, Vol. 18, 330331, 1975
[Dijkstra 76]
Dijkstra, E.: “A Discipline of Programming”, Prentice Hall, 1976
[Gabriel 85]
Gabriel, J.; Lindholm, T.; Lusk, E.; Overbeek, R.: “A Tutorial on the Warren
Abstract Machine”, Argonne National Laboratory, 1985
[Goldberg 83]
Goldberg, A.; Robson D.: “Smalltalk-80, The Language and Its Implementation”,
Addison Wesley, 1983
[Goldberg 83]
Goldberg, A.:“Smalltalk-80,
Addison Wesley, 1983
The
Interactive
Programming
Environment”,
[Goodwin 81]
Goodwin, J.: “Why Programming Environments Needs Dynamic Data Types”, IEEE
Transactions on Software Engineering, val.SE-7,No.5, 1981
[Emden 81]
Emden, M.: “An Algorithm for Interpreting Prolog Programs”, Department of
Computer Science, University of Waterloo, Ontário, Canada, Research Report CS81-28, Setembro 1981
[Green 68]
Green, C.: “Theorem-Proving by Resolution as a Basis for Question-Answering
Systems”, Machine Intelligence 4, B. Meltzer e D. Michie (Eds.), Edinburgh
University Press, 1968
[Hermenegildo 89]
Hermenegildo, M.: “High-Performance Prolog Implementation: the WAM and
Beyond”, Tutorial, 6º ICLP, Lisboa, Portugal, 1989
[Hogger 84]
Hogger, C.J.: “Introduction to Logic Programming”, Academic Press, 1984
[Janssens 88]
Janssens, G.; Demoen, B.; Marien, A.: “Improving the Register Allocation in WAM
by reordering unification”, em Proc. 5º ICLP, USA, MIT Press, 1988
[Kerninghan 87]
Kerninghan B.; Ritchie D. "The C programming language". Prentice-Hall, 1987
[Klint 81]
Klint, P.: “Interpretation Techniques”, em Software - Practice and Experience,
vol.11, 1981
[Kluzniak 85]
Kluzniak, F.; Bien J.: “Prolog for Programmers”, Academic Press, 1985
Bibliografia
138
[Kowalski 74]
Kowalski, R.; van Emden: “The Semantic of predicate logic as programming
language”, DCL memo 73, University of Edinburgh, 1974
[Kowalski 79a]
Kowalski, R.: “Logic for Problem Solving”, North-Holland, 1979
[Kowalski 79b]
Kowalski, R.: “Algorithm = Logic + Control”, Communications of the ACM, 22,
1979
[Krasner 83]
Krasner, G.: “Smalltalk-80, Bits of History, Words of Advice”, Addison Wesley,
1983
[Lamma 89]
Lamma, E.; Mello P.; Natali A.: “The design of an Abstract Machine for Efficient
Implementation of Contexts in Logic Programming”, em Proc. 6º ICLP, Lisboa,
Portugal, MIT Press, 1989
[Lloyd 87]
Lloyd, W.: “Foundations of Logic Programming”, Springer-Verlag, 1987
[Maier 88]
Maier, D.; Warren, D. S.: “Computing with Logic”, Benjamin Cummings Publishing
Company, 1988
[Mamede 89]
Mamede M.: “Programação em Lógica por Restrições”, Report 39/89 - DI/UNL,
Universidade Nova de Lisboa, 1989
[Martelli 82]
Martelli, A.; Montanari, U.: “An Efficient Unification Algorithm”,
Transactions on Programming Languages and Systems, 4, 2, 1982
ACM
[Mellish 82]
Mellish, C.: “An Alternative to Structure Sharing in the Implementation of a
Prolog Interpreter”, em Logic Programming, Academic Press, 1982
[Mellish 85]
Mellish, C.: “Some Global Optimizations for a Prolog Compiler”, em Journal of
Logic Programming, 1, 1985
[Miller 86]
Miller, D.: “A Theory of Modules for Logic Programming”, em Proc. Symp. on Logic
Programming, Salt Lake City (USA), 1986
[Monteiro 89a]
Monteiro, L.; Porto A.: “Contextual Logic Programing”, em Proc. 6º ICLP, Lisboa,
Portugal, MIT Press, 1989
[Monteiro 89b]
Monteiro, L.; Pedro. V.: “Cactus - a CxLP Preprocessor”, Internal Report - DI/UNL,
Universidade Nova de Lisboa, 1989
[Monteiro 89c]
Monteiro, L.; Porto A.: “Description of a Basic System for Contextual Logic
Programing”, Report 36/89 - DI/UNL, Universidade Nova de Lisboa, 1989
Bibliografia
139
[Moss 86]
Moss, C.: “CUT & PASTE - defining the impure Primitives of Prolog”, em Proc. 3º
ICLP, London, Springer-Verlag, 1986
[Odette 87]
Odette, L.: “How to Compile Prolog”, em AI Expert, Agosto 1987
[Pereira 80]
Pereira, L.M.; Porto A.: “An Interpreter for Logic Programs using Selective
Backtracking”, Report 3/80, Centro de Informática da Universidade Nova de
Lisboa, 1980
[Pereira 82]
Pereira, L.M.; Porto A.: “Selective backtracking for logic programs”, em Logic
Programming, Academic Press, 1982
[Plotkin 81]
Plotkin, G.D.: “A Structural Approach to Operational Semantics”, DAIMI
Technical Report FN-19, Computer Science Department, Aarhus University, 1981
[Porto 87]
Porto A., Pereira L. M., Trindade L.: “UNL Prolog User’s Guide Manual”,
Departamento de Informática, Universidade Nova de Lisboa, Maio de 1987
[Robinson 65]
Robinson, J.: “A Machine-Oriented Logic Based on the Resolution Principle”,
Journal of the ACM 12, 23-41, 1965
[Robinson 80]
Robinson, J.; Sibert E.: “Logic Programming in Lisp”, Research Report, School of
Computer and Information Science, Syracuse University, New York, 1980
[Roussel 75]
Roussel, P.: “Prolog, Manuel de Référence et d'Utilisation”, Groupe d'Intelligence
Artificielle, Université d'Aix-Marseille, 1975
[Russinoff 89]
Russinoff, D.: “A Verified Prolog Compiler for the Warren Abstract Machine”, MCC
Technical Report ACT-ST-292-89, Microelectronics and Computer Technology
Corp., Austin, USA, Julho 89
[Sterling 86]
Sterling, L.; Shapiro E.: “The Art of Prolog”, MIT Press, 1986
[Tick 84]
Tick, E.; Warren, D.: “Towards a Pipelined Prolog Processor”, em Proc. Symp. on
Logic Programming, Atlantic City (USA), 1984
[Teitelman 81]
Teitelman, W; Masinter L.: “The Interlisp Programming Environment”, IEEE
Computer, 1981
[Teitelman 84]
Teitelman, W: “A Tour Through Cedar”, IEEE Software, 1984
[Turk 86]
Turk A.: “Compiler Optimizations for the WAM”, em Proc. 3º ICLP, London,
Springer-Verlag, 1986
Bibliografia
140
[van Emden 76]
van Emden, M; Kowalskli, R.: “The semantics of predicate logic as a programming
language”, Journal of the ACM 23(4), 1976
[Warren 74]
Warren, D.: “Warplan: A System for Generating Plans”, Memo 76, University of
Edinburgh, 1974
[Warren 77a]
Warren, D.: “Implementing Prolog: compiling predicate logic programs, DAI Report
39-40, University of Edinburgh,1977.
[Warren 77b]
Warren, D.; Pereira, L.M.; Pereira F.: “Prolog - The Language and its
Implementation Compared with Lisp”, em Proc. of the Symposium on Artificial
Intelligence and Programming Languages, SIGPLAN Notices, Vol 12, N. 8, e SIGART
Newsletters, Nº 64, Agosto 1977
[Warren 80]
Warren, D.: “Logic Programming and Compiler Writing”, Software - Practice and
Experience, Vol.10, 1980
[Warren 83]
Warren, D.: “An Abstract Prolog Instruction Set”, SRI Technical Note 309, SRI
International, 1983
[Warren 88]
Warren, D.: “Implementation of Prolog”, Lecture notes, Tutorial nº 3, Symp. on
Logic Programming, Seattle, USA, 1988
Download

implementação de um sistema de programação em lógica