UNIVERSIDADE CATÓLICA DE PELOTAS ESCOLA DE INFORMÁTICA CURSO DE CIÊNCIA DA COMPUTAÇÃO PROJAVA: Um Tradutor de Prolog para Java por AUGUSTO MECKING CARINGI Projeto de Graduação submetido como requisito parcial à obtenção do grau de Bacharel em Ciência da Computação. Orientador: Prof. Jorge L. V. Barbosa Co-Orientadores: Prof. Adenauer C. Yamin Prof. Luiz A. M. Palazzo Pelotas, Novembro de 2002 2 Dedico, in memoriam, à minha avó, Noemi de Assumpção Osorio Caringi. 3 Quem, de três milênios, Não é capaz de se dar conta Vive na ignorância, na sombra, À mercê dos dias, do tempo. Johann Wolfgang von Goethe 4 SUMÁRIO LISTA DE FIGURAS 6 LISTA DE TABELAS 7 RESUMO 8 ABSTRACT 9 1 INTRODUÇÃO 10 1.1 O Contexto do Trabalho . . . . . . . . . . . . . . . . . . . . . . . . . 12 1.2 Principais Contribuições do Autor . . . . . . . . . . . . . . . . . . . . 13 1.3 Estrutura do Texto . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 2 HOLOPARADIGMA 15 2.1 Contexto e Motivação . . . . . . . . . . . . . . . . . . . . . . . . . . 15 2.2 Hololinguagem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16 2.3 Holojava . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17 3 PROLOG 3.1 Origens . . . . . . . . . . . . . . 3.2 Implementação . . . . . . . . . . 3.3 O Modelo de Execução Prolog . . 3.4 WAM: Warren Abstract Machine 3.5 Técnica de Binarização . . . . . . 3.6 Sistemas Prolog em Java . . . . . 3.6.1 jProlog . . . . . . . . . . . 3.6.2 Prolog Café . . . . . . . . . . . . . . . . 19 19 19 19 20 21 22 22 23 4 PROJAVA 4.1 Visão Geral . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2 Estrutura de um Tradutor . . . . . . . . . . . . . . . . . . . . . . . . 4.3 JavaCC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24 24 24 26 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 4.4 4.5 4.6 JJTree . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 Definição da Gramática Prolog . . . . . . . . . . . . . . . . . . . . . 29 Árvore de Sintaxe Abstrata . . . . . . . . . . . . . . . . . . . . . . . 31 5 IMPLEMENTAÇÃO 5.1 Ambiente de Execução do Prolog Café 5.2 Indexação de Cláusulas . . . . . . . . . 5.3 Funcionamento . . . . . . . . . . . . . 5.4 Estrutura do Código Traduzido . . . . 5.5 Benchmarks . . . . . . . . . . . . . . . 5.6 Limitações . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 33 34 34 35 38 39 6 CONCLUSÕES 40 7 REFERÊNCIAS BIBLIOGRÁFICAS 42 A Gramática Prolog para o JavaCC 45 B Programas Prolog usados nos B.1 Árvore Genealógica . . . . . B.2 Série de Fibonacci . . . . . . B.3 Manipulação de Listas . . . B.4 Torres de Hanói . . . . . . . 49 49 49 49 50 benchmarks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . C Exemplo de Código Traduzido: Fibonacci 51 D Exemplo de Holoprograma 55 6 LISTA DE FIGURAS FIGURA 1 FIGURA 2 Polı́tica de Conversão de Holo para Java . . . . . . . . . . . 18 Gerenciamento de Arquivos na Conversão de Holo para Java 18 FIGURA 3 FIGURA 4 Módulos do Prolog Café . . . . . . . . . . . . . . . . . . . . 23 Diagrama de Execução . . . . . . . . . . . . . . . . . . . . 23 FIGURA 5 FIGURA 6 FIGURA 7 Diagrama de Geração do Parser . . . . . . . . . . . . . . . 29 Gramática Prolog . . . . . . . . . . . . . . . . . . . . . . . 31 Exemplo de Árvore de Sintaxe Abstrata . . . . . . . . . . . 32 FIGURA 8 FIGURA 9 Funcionamento Interno do PROJAVA . . . . . . . . . . . . 35 Diagrama de Tradução de Prolog para Java . . . . . . . . . 35 7 LISTA DE TABELAS TABELA 1 TABELA 2 Principais Opções do JavaCC . . . . . . . . . . . . . . . . . 27 Tipo de Produções do JavaCC . . . . . . . . . . . . . . . . 28 TABELA 3 TABELA 4 Classes Java que modelam os termos Prolog . . . . . . . . . 33 Benchmarks . . . . . . . . . . . . . . . . . . . . . . . . . . 38 8 RESUMO O Holoparadigma foi desenvolvido com as seguintes motivações: exploração do paralelismo implı́cito, atuar sobre arquiteturas distribuı́das, trabalhar com múltiplos paradigmas e explorar a execução dos múltiplos paradigmas de forma paralela e distribuı́da. Com o objetivo de validar o modelo no menor prazo de tempo possı́vel, foi implementada uma ferramenta para tradução de código Holo para código Java. Algumas abstrações da Hololinguagem podem ser traduzidas diretamente para Java; outras, entretanto, necessitam de programas adicionais. Este trabalho se enquadra nesse contexto e contribuirá para integração da programação em lógica ao Holoparadigma. Seu objetivo é a implementação de um módulo para tradução de Prolog para Java. Este módulo será responsável pela tradução das ações modulares lógicas da Hololinguagem em código Java, substituindo o módulo utilizado anteriormente, o qual apresenta sérios problemas de desempenho. Palavras-chave: Holoparadigma, Holojava, Prolog, Técnicas de tradução Prolog, Programação Multiparadigma 9 ABSTRACT Holoparadigm was developed with the following motivations: to exploit the implicit parallelism, to be run in distributed architectures, to work with multiple paradigms and to explore the execution of multiple paradigms in a parallel and distributed manner. With the goal of validating the model in the shortest time possible, was implemented a tool for the translation of Holo code to Java. Some abstractions of Hololanguage could be translated direct to Java, others however need additional programs. This work is fit in this context and will contribute to the integration of logic programming to Holoparadigm. Its goal is the implementation of a module to translate Prolog to Java. This module will be responsible for the translation of Hololanguage’s modular logic actions to Java code, replacing the previously used module which have serious performance problems. Keywords: Holoparadigm, Holojava, Prolog, Prolog translation techniques, Multiparadigm Programming 10 1 INTRODUÇÃO Maior produtividade na programação pode ser obtida através da escolha do paradigma de programação mais apropriado ao tratamento do problema, entretanto, no desenvolvimento de sistemas de software complexos, muitas vezes, um único paradigma não é suficiente. Nesses casos, a melhor abordagem é a programação multiparadigma (HORSPOOL 1996). Linguagens de programação tradicionais (baseadas no paradigma imperativo) são criticadas por não possuı́rem embasamento matemático, pela dificuldade que elas trazem ao desenvolvimento de software, por não possuirem clareza e por forçarem o programador a pensar de maneira seqüencial. Por outro lado, quase a totalidade de sistemas implementados para uso industrial e comercial são desenvolvidos em linguagens imperativas, ficando os paradigmas “alternativos” restritos ao meio acadêmico. Provavelmente isso se atribua ao fato de que a transição-de-estados, conceito fundamental da programação imperativa, seja, na verdade, um paradigma muito útil para resolver problemas práticos. Contudo, torna-se claro que certos tipos de problemas são resolvidos de muito melhor forma através de paradigmas não-imperativos. “Resolvidos de muito melhor” forma significa que a solução é mais clara, mais legı́vel, mais concisa e que ele modela de uma maneira muito melhor o problema tratado (HORSPOOL 1996). Um dos principais representantes dos “paradigmas não-convencionais” é o paradigma da programação em lógica, paradigma este que tem como modelo computacional a lógica do cálculo de predicados; conforme COLMERAUER (1992), o termo “programação em lógica” é devido a Robert Kowalski, que vislumbrou a possibilidade de utilização da lógica como linguagem de programação. Prolog, por sua vez, é a linguagem pioneira e principal representante do paradigma da programação em lógica. De acordo com COLMERAUER (1992), Prolog, que é a abreviação de “PROgrammation en LOGique”, foi desenvolvida no inı́cio da década de 70 por ele e seus colegas, na Universidade de Marselha. Desde 11 então, a linguagem foi alvo de intensas pesquisas e, entre as suas principais áreas de aplicação, destacam-se: • Sistemas baseados em conhecimento; • Sistemas de bases de dados; • Sistemas especialistas; • Processamento da linguagem natural; • Engenharia de software; • Prova automática de teoremas; • Construção de compiladores; • Computação simbólica. Segundo STERLING (1994), um programa lógico é um conjunto de axiomas, ou regras, que definem relações entre objetos. A computação de um programa lógico é uma dedução de conseqüências do programa. Um programa define um conjunto de conseqüências, que são seu significado. A arte da programação em lógica consiste em construir programas concisos e elegantes que apresentem o significado desejado. Umas das principais idéias da programação em lógica é de que um algoritmo é constituı́do por dois elementos disjuntos: a lógica e o controle. O componente lógico corresponde à definição do que deve ser solucionado, enquanto o componente de controle estabelece como a solução pode ser obtida. O programador precisa somente descrever o componente lógico de um algoritmo, deixando o controle da execução a cargo do sistema computacional. Isto possibilita que o programador simplesmente especifique o problema, ao invés de ter que descrever o procedimento para solucioná-lo (PALAZZO, 1997). Este alto poder de expressão, aliado à capacidade de exploração do paralelismo implı́cito da linguagem Prolog, a torna umas das peçaschave do Holoparadigma. HORSPOOL (1996) e BARBOSA (2002) analisam as diferentes técnicas de programação multiparadigma. A Hololinguagem é classificada como uma linguagem 12 multiparadigma de unificação total, porém, com o objetivo de testar os holoprogramas no menor espaço de tempo possı́vel, foi desenvolvida uma ferramenta que traduz código Holo para código Java, neste caso, a abordagem utilizada para fins de execução consiste na programação multiparadigma baseada em tradução, posto que os diferentes paradigmas que compõem o Holo são traduzidos para o paradigma imperativo/orientado a objetos da linguagem Java. A ferramenta Holojava, a qual tem seu funcionamento detalhado no capı́tulo 2, realiza a tradução dos holoprogramas para Java através de um mapeamento das construções Holo para construções Java. Entretanto, alguns aspectos não possuem equivalentes em Java; nesses casos, são utilizadas ferramentas adicionais, Tratandose especificamente do código Prolog, a ferramenta utilizada até então para tradução era o Prolog Café, contudo, o baixo desempenho deste software, bem como a necessidade do domı́nio desta tecnologia de tradução, foram os fatores motivadores do presente trabalho. 1.1 O Contexto do Trabalho O Holoparadigma (BARBOSA, 2001) foi desenvolvido com as seguintes motivações: exploração do paralelismo implı́cito, atuar sobre arquiteturas distribuı́das, trabalhar com múltiplos paradigmas e explorar a execução destes de forma paralela e distribuı́da. Este trabalho se enquadra neste contexto e tem por objetivo central a implementação de um novo tradutor de código fonte Prolog para código fonte Java, com o intuito de substituir o utilizado anteriormente, o qual possui sérios problemas de desempenho. O foco do trabalho é a integração do paradigma da programação em lógica ao Holoparadigma, através do novo tradutor. 13 1.2 Principais Contribuições do Autor Além de participar de discussões sobre os aspectos relativos à integração do paradigma da programação em lógica ao Holoparadigma, destaca-se a atuação do autor nos seguintes aspectos do trabalho: • Identificação da origem do problema de desempenho do tradutor utilizado posteriormente; • Definição da melhor abordagem para sanar o gargalo de desempenho; • Identificação da necessidade de aproveitamento do ambiente de execução do Prolog Café; • Implementação do novo tradutor. 1.3 Estrutura do Texto O presente trabalho está dividido em seis capı́tulos, de forma a introduzir o leitor gradativamente no tema proposto. O primeiro capı́tulo é a introdução, onde foram apresentados os objetivos, o contexto do do trabalho e também as contribuições do autor. No segundo capı́tulo, são introduzidos conceitos referentes ao Holoparadigma e tecnologias relacionadas, incluindo Holojava, a ferramenta de tradução de Holo para Java. O terceiro capı́tulo trata da linguagem Prolog, sua história, suas principais caracterı́sticas, fundamentos e técnicas de implementação, além do tradutor atualmente utilizado na Holojava: Prolog Café. Nos capı́tulo quatro e cinco, são descritos respectivamente, o modelo proposto para o novo tradutor e sua implementação e, no sexto, estão as conclusões deste trabalho. 14 A gramática Prolog no formato da ferramenta JavaCC utilizada no desenvolvimento do tradutor PROJAVA está no anexo A. No anexo B, por sua vez, estão os programas em Prolog utilizados nos testes de tradução e benchmarks. A listagem completa do programa que calcula Fibonacci traduzido para Java pode ser encontrada no anexo C e, finalmente, o anexo D contém um exemplo de programa na Hololinguagem. 15 2 2.1 HOLOPARADIGMA Contexto e Motivação Conforme BARBOSA (2002), o Holoparadigma (de forma abreviada, Holo) é um modelo multiparadigma que possui uma semântica simples e distribuı́da. Através dessa semântica, o modelo estimula a exploração automática da distribuição (distribuição implı́cita). O estı́mulo à distribuição implı́cita é seu principal objetivo. O paralelismo implı́cito propõe a detecção automática do paralelismo e sua exploração através de mecanismos inseridos no software básico dos sistemas computacionais. Por sua vez, nos últimos anos, os sistemas distribuı́dos têm recebido cada vez mais atenção, tanto dos centros de pesquisa quanto das empresas. Nesse contexto, torna-se interessante a integração dos temas paralelismo implı́cito e sistemas distribuı́dos em um tópico de pesquisa denominado distribuição implı́cita (BARBOSA, 2002). O Holoparadigma possui como unidade de modelagem os entes e, como unidade de informação, os sı́mbolos. Um ente elementar é organizado em três partes: interface, comportamento e história. Um ente composto possui a mesma organização de um ente elementar, no entanto, suporta a existência de outros entes na sua composição (entes componentes). Cada ente possui uma história. A história fica encapsulada no ente e, no caso dos entes compostos, é compartilhada pelos entes componentes. De acordo com BARBOSA (2002), um ente elementar assemelha-se a um objeto do paradigma orientado a objetos. Do ponto de vista estrutural, a principal diferença consiste na história, a qual atua como uma forma alternativa de comunicação e sincronização. Um ente composto assemelha-se a um grupo. Neste caso, a história atua como um espaço compartilhado vinculado ao grupo. 16 2.2 Hololinguagem Conforme BARBOSA (2002), a Hololinguagem (de forma abreviada Holo), implementa os conceitos propostos pelo Holoparadigma. Suas principais caracterı́sticas são: • multiparadigma: Holo integra múltiplos paradigmas básicos. Atualmente, estão integrados os paradigmas imperativo, em lógica e orientado a objetos; • sem tipos e orientada ao processamento simbólico: o sı́mbolo é a unidade de informação do Holoparadigma. A Hololinguagem é baseada no processamento de sı́mbolos e não possui tipos de dados; • integração da atribuição e unificação: as variáveis lógicas são manipuladas através da unificação (paradigma em lógica), mas também existe suporte para atribuição; • programação de alta ordem: Holo suporta programação de alta ordem, isto significa que sı́mbolos representandos entes e suas partes podem ser manipulados através de variáveis; • blackboard hierárquico: exite suporte a vários nı́veis de blackboard; • suporte à Holoclonagem: Holo implementa a clonagem múltipla e seletiva como proposto no Holoparadigma; • mobilidade: a mobilidade lógica é suportada explicitamente, já a mobilidade fı́sica fica a cargo do ambiente de execução; • suporte à concorrência: existe suporte à concorrência entre ações e entre entes; • sintaxe lógica compatı́vel com Prolog: os aspectos lógicos da sintaxe da linguagem são compatı́veis com a linguagem Prolog; • sintaxe imperativa baseada no Pascal: os aspectos imperativos da sintaxe são baseados no Pascal, pela sua simplicidade e algumas semelhanças sintáticas com Prolog. 17 Um holoprograma é formado por descrições de entes (entes estáticos), as quais são formadas por Cabeça e Corpo. A cabeça contém o nome de um ente, seus argumentos e uma descrição de sua clonagem. O corpo é formado por duas partes: Comportamento e História. A história é um blackboard que pode ser acessado pelas ações, pelos entes componentes ou pela própria história. O comportamento é constituı́do por um conjunto de ações que implementam a funcionalidade, existindo cinco tipos diferentes: • Ação Lógica (Logic Action - LA): ação formada por um único predicado lógico (uma ou mais cláusulas com mesmo nome e aridade); • Ação Imperativa (Imperativa Action - IA): ação formada por uma função imperativa; • Ação Modular Lógica (Modular Logic Action - MLA): ação formada por um módulo que encapsula ações lógicas; • Ação Modular Imperativa (Modular Imperative Action - MIA): ação formada por um módulo que encapsula ações imperativas; • Ação Multiparadigma (Multiparadigm Action - MA): ação que suporta o encapsulamento de LAs e IAs. 2.3 Holojava De acordo com BARBOSA (2001), visando à realização de testes de Holoprogramas (programas em Holo) no menor espaço de tempo possı́vel, foi criada uma ferramenta de conversão de programas, denominada HoloJava. Esta ferramenta converte programas em Holo (linguagem origem) para programas em Java (linguagem destino). Esta técnica é bastante usada na avaliação de novas linguagens, pois a utilização de uma linguagem destino que possua uma plataforma (compilação e execução) já disponı́vel antecipa a realização de testes e a difusão de resultados da pesquisa. Diversos estudos indicam que Java pode ser utilizada como linguagem intermediária na construção de compiladores. Java suporta diretamente várias abstrações 18 do Holoparadigma (ações imperativas, concorrência, etc). No entanto, algumas caracterı́sticas de Holo não são suportadas (ações lógicas, blackboard e mobilidade). Por outro lado, existem bibliotecas que complementam a plataforma Java e podem ser utilizadas para implementação dos aspectos não suportados. Por exemplo, ações lógicas em Prolog Café (BANBARA, 1999), história em Jada ou JavaSpaces e mobilidade fı́sica em Voyager ou Horb. A figura 1 mostra detalhadamente esta polı́tica de conversão. Holo Java Ente estático Classe Clonagem de Transição suportando concorrência inter−ente (ação clone) Criação de um objeto (comando new) encapsulado em uma thread e atualização da HoloTree Mobilidade lógica (ação move) Reorganização da HoloTree e controle de membership nos entes envolvidos Ação Imperativa (IA) Método Ação Modular Lógica (MLA) Classes em Java (Prolog Café) Invocação explícita Mensagem História Espaço de objetos em Jada Afirmação para a história (símbolo "!") Inserção de um fato no espaço Jada (método "out") Pergunta não destrutiva bloqueante para a história (símbolo ".") Obtenção de um fato no espaço Jada (método "read") Pergunta destrutiva bloqueante para a história (símbolo "#") Obtenção de um fato no espaço Jada (método "in") Pergunta não−bloqueante e não− destrutiva para a história (símbolo "?") Obtenção de um fato no espaço Jada (método "read_nb") Pergunta não−bloqueante destrutiva para a história (símbolo "?#") Obtenção de um fato no espaço Jada (método "in_nb") FIGURA 1: Polı́tica de Conversão de Holo para Java A figura 2 ilustra como é feito o gerenciamento dos arquivos na conversão de Holo para Java. É utilizado no exemplo o programa datamining.holo, o qual é composto de 3 entes e uma ação modular lógica (MLA); a listagem completa do 19 programa está no anexo D. datamining.holo HoloJava 1 Ente holo holo.java Ente mine mine.java Ente miner MLA fib miner.java fib.pl 2 Prolog Café Predicado fib/2 PRED_fib_2.java 3 Compilador Java holo.class + *.class 4 JVM FIGURA 2: Gerenciamento de Arquivos na Conversão de Holo para Java Através de benchmarks realizados em BARBOSA (2001a), foi possı́vel constatar o baixo desempenho do Prolog Café. Em um programa Holo de 73 linhas, das quais apenas 12 compondo uma MLA, 98,93% do tempo total de conversão foi gasto pelo Prolog Café na tradução da MLA, e os resultados se repetiram em diferentes programas, a conversão de um programa sem MLAs chegou a ser 105 vezes mais rápida se comparada a um programa com MLAs. Baseado em tal fato, a utilização das ações modulares lógicas foi considerada um ponto crı́tico no desempenho da conversão. 20 3 3.1 PROLOG Origens O nome “Prolog”, abreviação de “PROgrammation en LOGique”, foi inventado em Marselha no ano de 1972, ele foi escolhido por Philippe Roussel para designar um software projetado para implementar um sistema de comunicação homemmáquina em linguagem natural (COLMERAUER, 1992). A idéia chave por trás da programação em lógica é que, computação pode ser expressa como dedução controlada. 3.2 Implementação A implementação de Prolog tem uma longa história. Os primeiros sistemas foram implementados pelo grupo em torno de Colmerauer, em Marselha, sendo que o primeiro interpretador foi escrito em 1972, em Algol. Se, por um lado, foi demonstrado que era possı́vel a implementação de uma linguagem baseada na lógica do cálculo de predicados, por outro, foi constatado que o principal problema era o baixo desempenho. O primeiro compilador Prolog foi escrito por David Warren em conjunto com Fernando e Luı́s Pereira em 1977. Esse compilador provou que era possı́vel executar Prolog com desempenho semelhante ao das linguagens imperativas tradicionais e contribuiu muito para o sucesso da linguagem Prolog. 3.3 O Modelo de Execução Prolog As duas partes básicas de um interpretador Prolog são a parte de unificação e a de resolução. A resolução é bastante simples, ela consiste em um mecanismo de resolução-SLD simplificado, que procura as cláusulas de cima para baixo e avalia os objetivos da esquerda para a direita. Esta estratégia leva ao mecanismo de retrocesso (backtracking) e ao layout usual das áreas de dados e pilha. A resolução manipula a 21 pilha de alocação, chamada de procedimentos, e o backtracking. Unificação em Prolog é definida como a seguir: • duas constantes unificam se elas são iguais; • duas estruturas unificam se seus functores (nome e aridade) são iguais e todos os argumentos unificarem; • duas variáveis livres unificam e são amarradas juntas; • uma variável livre e uma constante ou estrutura unificam e a constante ou estrutura é amarrada à variável. 3.4 WAM: Warren Abstract Machine Seis anos após o desenvolvimento de seu compilador, David Warren apresentou um novo conjunto de instruções abstratas Prolog. Este novo Prolog Engine veio a se tornar muito popular com o nome Warren Abstract Machine (WAM). Desde então, ele tem sido a base para praticamente todas as implementações Prolog a partir do ano de 1983. O Objetivo da WAM é de servir como um modelo simples e eficiente de implementação para interpretadores de byte code, bem como geradores de código nativo. GRUNE et al. (2001) analisam o fato de que enquanto poucos códigos intermediários para linguagens imperativas alcançaram mais que fama local, o código WAM se tornou um padrão de fato, e isso pode refletir a complexidade da tarefa de compilar Prolog. O modelo de execução da WAM é mais semelhante ao das linguagens imperativas do que qualquer outro modelo. A principal idéia é a divisão da unificação em duas partes, a cópia dos argumentos do objetivo chamador para registradores de argumentos e então a unificação destes registradores de argumentos com a cabeça da cláusula chamada. Isto é muito semelhante à passagem de parâmetros para funções em linguagens imperativas como C. 22 De acordo com GRUNE et al. (2001), as instruções da WAM lidam com itens como a seleção da cláusula adequada a usar na dedução de um fato, unificação sob várias circunstâncias e backtracking. Além disso, a WAM faz toda a alocação de memória, empregando, para isso, cinco pilhas. 3.5 Técnica de Binarização Conforme LINDGREN (1994), um problema fundamental em executar Prolog, consiste em como mapear eficientemente o controle (incluindo recursão e backtracking) para um hardware convencional. A solução tradicional para este problema, é traduzir código Prolog em instruções para uma máquina abstrata que gerencie as estruturas de controle da linguagem Prolog. Tipicamente isso é realizado através da WAM ou uma variante dela, outra abordagem, entretanto, é a técnica de binarização. A idéia chave do “Prolog Binário” é a transformação das cláusulas convencionais em cláusulas binárias utilizando Continuation Passing Style (CPS). A essência da técnica de CPS consiste em inserir um argumento adicional em cada predicado, que representa o próximo predicado a ser executado em caso de sucesso. Pesquisas na área de linguagens funcionais demonstraram que a técnica de CPS pode expressar o controle de uma computação de uma maneira muito semelhante ao que os gráficos de controle de fluxo fazem para as linguagens imperativas (LINDGREN, 1994). BinProlog, um sistema Prolog muito eficiente desenvolvido por Paul Tarau (TARAU, 1990) demonstrou as vantagens e desvantagens desta abordagem de implementação. Em “Prolog Binário”, uma cláusula tem no máximo um subobjetivo, dessa forma, a regra de controle da resolução-SLD que era implı́cita, tornase explı́cita. O código a seguir ilustra como uma cláusula normal pode ser transformada em uma cláusula binária. nrev([],[]). nrev([H|T],R) :nrev(T,L), append(L,[H],R). 23 nrev([],[],Cont) :- call(Cont). nrev([H|T],R,Cont) :nrev(T,L, append(L,[H],R,Cont)). KRALL (2002) também analisa os prós e contras deste método de implementação, o qual é motivado pelas simplificações que se tornam possı́veis no sistema de execução. BinProlog, por exemplo, utiliza uma versão muito simplificada da WAM, entretanto, algumas desvantagens, como maior consumo de memória, se tornam presentes. 3.6 Sistemas Prolog em Java A implementação de sistemas eficientes Prolog em Java é atualmente alvo de pesquisas (BANBARA, 1999). O número de sistemas deste tipo cresce a cada dia: BirdLand’s Prolog in Java, CKI Prolog, DGKS Prolog, JavaLog, Jinni, JP, jProlog, LL, MINERVA, W-Prolog e Prolog Café. Destes, apenas jProlog e Prolog Café são tradutores, os outros são interpretadores. 3.6.1 jProlog jProlog foi o primeiro conversor de Prolog para Java, desenvolvido em 1996 por Bart Demoen e Paul Tarau, ele se baseia na técnica binarização (TARAU, 1990). O sistema consiste em um conjunto de classes Java que dão suporte à execução, incluindo uma série de predicados built-in e um tradutor escrito em Prolog. O tradutor não suporta o bootstraping de forma que é necessário um sistema Prolog (SICSTus por exemplo) para realizar a conversão. Para executar o código, uma vez que ele tenha sido traduzido, basta uma máquina virtual Java. Conforme informações obtidas no site do projeto, JPROLOG (2002), o tradutor foi escrito em Prolog por dois motivos: a falta de um gerador de parsers em Java eficiente na época do projeto (1996), e as facilidades oferecidas pela linguagem Prolog para tal tipo de tarefa, incluı́ndo a extensão DCG (Gramática de Cláusulas Definidas). 24 3.6.2 Prolog Café Conforme BANBARA (1999), Prolog Café é uma ferramenta que traduz código fonte de um superconjunto de Prolog, chamado LLP (Linear Logic Programming Language), para código fonte Java. O programa foi desenvolvido por Mutsunori Banbara e Naoyuki Tamura, tendo por base o jProlog. Benchmarks também realizados por BANBARA (1999) demonstram que o código gerado é 2,2 vezes mais rápido se comparado ao código do jProlog. Além do tradutor, o programa consiste de um ambiente de execução para o código traduzido, o qual é fortemente inspirado na WAM e um conjunto de predicados built-in alguns escritos diretamente em Java e outros traduzidos a partir do Prolog. A figura 3 ilustra os módulos da ferramenta, e a figura 4, por sua vez, mostra como é feita a execução. O sistema possui duas versões do mesmo tradutor, ambos com a mesma funcionalidade, um implementado em Prolog e outro em Java, este último sendo obtido através da técnica de bootstraping, isto é, o tradutor em Prolog é usado como entrada para ele próprio, obtendo como saı́da um tradutor em Java. De acordo com AHO (1995), bootstraping consiste em utilizar as facilidades oferecidas por uma linguagem para compilar a si mesma. Prolog Café Ambiente de Execução para o código traduzido Tradutor de Prolog para Java escrito em Prolog (acompanha versão bootstrapped) Classes que dão suporte a execução (termos, algoritmo de unificação, etc.) Predicados Built−in escritos em Java FIGURA 3: Módulos do Prolog Café 25 Ambiente de Execução Código Traduzido Conjunto de classes Java que de Prolog para Java dão suporte a execução do código traduzido Máquina Virtual Java (JVM) Sistema Operacional FIGURA 4: Diagrama de Execução 26 4 4.1 PROJAVA Visão Geral Uma vez identificado que o baixo desempenho da ferramenta Prolog Café deve-se ao fato de o tradutor ser bootstrapped, e reconhecendo que o ambiente de execução da mesma é fruto de muitos anos de pesquisa relacionada à tradução de Prolog para linguagens imperativas, chegou-se à conclusão que a melhor abordagem seria reescrever o tradutor, a partir do zero, totalmente em Java, porém aproveitando as classes que dão suporte à execução do código traduzido. Para que isto seja possı́vel, basta que o novo tradutor seja capaz de gerar um código compatı́vel com o run-time environment já existente. Além disso, tal abordagem minimiza o impacto de adaptação à Holojava de um módulo de tradução de Prolog para Java totalmente diferente do que já vinha sendo utilizado. 4.2 Estrutura de um Tradutor O trabalho de um tradutor pode ser dividido basicamente em três partes: 1. Análise léxica 2. Análise Sintática 3. Geração do Código As primeiras duas fases compõem o núcleo do sistema e envolvem o entendimento do programa fonte e a verificação da correção léxica e sintática. A esse processo damos o nome de parsing. A função do analisador léxico é obter e dividir os tokens no programa fonte. Um token é uma peça significativa do fonte, como uma palavra chave, pontuação ou literais como números ou strings, trechos que são descartados incluem espaços em 27 branco e comentários. A análise sintática consiste em obter uma cadeia de tokens provenientes do analisador léxico e verificar se a mesma atende às especificações sintáticas da gramática da linguagem. Conforme AHO (1995) existem três tipos gerais de analisadores sintáticos: “método universal”, “top-down” e “bottom-up”. Através do método universal é possı́vel trabalhar com qualquer gramática, analisadores de tal classe são muito ineficientes. Os do tipo “top-down” e “bottom-up”, bem mais eficientes, trabalham somente com determinadas subclasses das gramáticas, mas que são, contudo, suficientemente expressivas para descrever as construções sintáticas das linguagens de programação. Para implementação, duas abordagens podem ser utilizadas, escrever o analisador léxico e o analisador sintático manualmente ou fazer uso de ferramentas que realizam a geração automática de tais tipos de software a partir de determinadas regras. Na década de 50, com o advento dos primeiros compiladores, a construção de analisadores, especialmente os sintáticos, era considerada uma tarefa árdua. Desde então, muita teoria foi desenvolvida na área, com grande destaque para a Teoria das Linguagens Formais, de modo que, atualmente, a construção manual é desconsiderada na maior parte dos casos, deixando-se tal tarefa para ferramentas automatizadas. Existe uma série de ferramentas deste tipo, sendo as mais conhecidas, os tradicionais lex (gerador de analisadores léxicos) e yacc (gerador de analisadores sintáticos) que advêm do mundo Unix; tais programas, porém, não geram código em Java. Dentro da gama de ferramentas que geram código Java, uma destacou-se como padrão: JavaCC (Java Compiler Compiler). É interessante observar que tal classe de software também é conhecida como Compilador de Compiladores. Outro fator que determinou a adoção do JavaCC foi a prévia utilização do mesmo no projeto Holoparadigma, posto que o parser da Hololinguagem é gerado através dele. Tendo o processo de parsing sido executado com sucesso e não gerado nenhum erro, o sistema terá uma representação interna do programa a qual pode ser facilmente manipulada pelo último estágio do tradutor: o gerador de código, que, ao contrário do parser, precisa ser escrito manualmente a fim de se obter o compor- 28 tamento desejado. 4.3 JavaCC JavaCC é um gerador de parsers “top-down” em Java. Essa classe de parsers também é conhecida como “descendente recursivo” e constrói a árvore de derivação a partir da raiz em direção às folhas, permitindo o uso de gramáticas mais gerais do que os parsers “bottom-up” que operam de forma inversa, sendo a sua única limitação a impossibilidade da utilização de produções com recursão à esquerda, o que poderia ocasionar recursão infinita (PANKAJ, 2002). A estrutura do tipo de analisador gerado é idêntica à especificação da gramática, o que pode ser visto como um fator a mais de facilidade. A ferramenta agrega as funcionalidades de gerador de analisador léxico e gerador de analisador sintático; dessa forma, o arquivo de especificação da gramática contém tanto as especificações léxicas quanto as sintáticas, tornando fácil a sua manutenção. Os recursos de análise léxica são semelhantes aos da ferramenta “lex”, sendo o “Token Manager” o componente responsável pelo reconhecimento dos tokens da gramática, seu funcionamento consiste em reconhecer e retornar os mesmos para o parser. Sua implementação é basicamente um autômato finito não-determinı́stico. Estrutura de um Arquivo de Gramática JavaCC: Opç~ oes JavaCC Parser_Begin(Identificador) Código Parser_End(Identificador) Produç~ oes A seção de opções é uma seção opcional onde são especificados vários parâmetros para customização do parser gerado, ela inicia pela palavra reservada options, 29 seguida por uma lista de uma ou mais opções. A tabela 1 lista as opções mais utilizadas. Nome da Opção LOOKAHED Descrição O valor padrão desta opção é 1, ela especifica o número de tokens que devem ser “olhados adiante” antes de ser tomada uma decisão em um ponto de escolha. STATIC Esta é uma opção booleana que tem como valor padrão verdadeiro. Ela implica que todos os métodos e variáveis de classe são definidos como estáticos no Token Manager e no parser DEBUG PARSER O valor padrão desta opção é falso, quando ativada ela faz o parser prover informações adicionais em tempo de execução. JAVA UNICODE ESCAPE O valor padrão é falso. Quando ativada, esta opção permite lidar com fluxos de entrada no formato Unicode. TABELA 1: Principais Opções do JavaCC A seção de Código, que fica entre Parser Begin( Identificador ) e Parser end( Identificador ), também é chamada de Java Compilation Unit e contém código Java opcional para instanciação do parser, caso ele não seja chamado através de uma classe externa. A última seção é a mais importante; uma gramática usada para especificar a sintaxe de uma linguagem de programação consiste em um conjunto de produções. Uma produção é uma regra pela qual uma seqüência de sı́mbolos terminais e nãoterminais são reduzidos a um não terminal, JavaCC permite a definição de quatro tipos de produções, conforme a tabela 2: 4.4 JJTree JJTree é um pré-processador para o JavaCC, que insere ações para criação de uma árvore de sintaxe abstrata (AST - Abstract Syntax Tree) em vários lugares, em 30 Tipo da Produção “Javacode” Descrição Em certos casos é difı́cil especificar construções livres de contexto para determinadas gramáticas, este tipo de produção permite inserir código Java ao invés de uma produção EBNF para lidar com estes casos especiais. “Expressão Regular” Estas produções são utilizadas para definir as entidades léxicas da gramática, também conhecidas como tokens, que serão posteriormente trabalhados pelo “Token Manager”. “EBNF” Esta é a maneira padrão de especificar gramáticas JavaCC. Uma produção EBNF possui o formato: NT → α, onde NT é um único sı́mbolo não-terminal e α é uma seqüência de zero ou mais terminais e/ou não-terminais. “Token Manager” As declarações nesta seção são embutidas no “Token Manager” gerado e são acessı́veis a partir das ações léxicas. TABELA 2: Tipo de Produções do JavaCC 31 uma gramática JavaCC. A saı́da do JJTree é então processada pelo JavaCC para criação do parser (JJTREE, 2002). A figura 5 ilustra esse processo. Com a simples especificação de uma gramática para o JavaCC, obtemos um parser, entretanto, tal software somente servirá para determinar se um dado programa atende ou não às regras da gramática. Para que o processo de parsing gere algum resultado (interpretação do programa, tradução para uma linguagem destino, etc.) é necessário programação adicional. O JavaCC permite a inserção de trechos de código Java associados a cada produção da gramática, tais códigos podem ter como objetivo a geração direta de um novo programa na linguagem destino. Outra abordagem, que traz maior flexibilidade, é a inserção de código para geração de uma estrutura de dados representando o programa fruto do parsing, a qual é conhecida como Árvore de Sintaxe Abstrata. O JJTree faz isto automaticamente, inserindo ações semânticas para criação da AST e gerando algumas classes que modelam a estrutura da mesma. Por padrão, para cada sı́mbolo não-terminal da gramática, o JJTree gera código para construção de um nodo na árvore. Por exemplo, um não-terminal chamado Term dará origem a uma classe chamada ASTTerm. Posteriormente, a árvore de sintaxe abstrata pode ser manipulada através de uma API do JJTree. 4.5 Definição da Gramática Prolog Foi utilizado um subconjunto da gramática Prolog definida pelo padrão ISO, a sintaxe completa da linguagem Prolog pode ser encontrada em SCOWEN (1993). A gramática Prolog é relativamente simples, pois não contém palavras reservadas. Um programa em lógica consiste basicamente de uma seqüência de cláusulas, por sua vez, cada cláusula pode ser um fato ou uma regra. Um fato possui apenas uma cabeça, enquanto uma regra possui uma cabeça e um corpo. O tipo de dado básico da linguagem Prolog é o “termo”, um termo pode ser uma constante, uma variável, uma lista ou um termo composto, uma constante, por sua vez, pode ser um átomo ou um número. 32 Gramática Prolog para o JAVACC JJTREE Gramática Prolog Anotada para o JAVACC JAVACC Programa Java que faz a análise léxica e sintática de acordo com as regras da gramática e gera uma AST FIGURA 5: Diagrama de Geração do Parser Uma das caracterı́sticas não suportadas na versão atual é a definição dinâmica de operadores. O padrão ISO Prolog especifica que novos operadores podem ser definidos em tempo de execução, linguagens como C++ permitem a redefinição de novos comportamentos semânticos para operadores já existentes, recurso conhecido como sobrecarga de operadores; entretanto, Prolog permite além disso, a utilização de qualquer sı́mbolo como novo operador. Uma vez que estes operadores não estão presentes nas regras gramaticais da linguagem, torna-se uma tarefa um pouco complexa seu tratamento. PANKAJ (2002) propõe um método para lidar com operadores dinâmicos com o JavaCC. Conforme GRUNE et al. (2001), as gramáticas livres de contexto constituem o formalismo essencial para descrever a estrutura de programas em uma linguagem de programação. De acordo com AHO (1995), tais gramáticas foram introduzidas por Chomsky como parte de um estudo das linguagens naturais e seu uso na especi- 33 ficação de linguagens de programação emergiu independentemente. O conceito de gramática livre de contexto, consistindo de sı́mbolos terminais, não-terminais, produções e sı́mbolo não-terminal inicial, independe da notação utilizada para escrever a gramática, contudo, uma notação se destacou, conhecida como Backus-Naur Form, ou simplesmente BNF, a qual foi utilizada pela primeira vez nos anos 60 para descrever a sintaxe da linguagem Algol (SETHI 1996). Posteriormente, foi desenvolvida uma extensão para a notação BNF, chamada EBNF (Extended Backus-Naur Form), permitindo a especificação de listas de elementos e elementos opcionais. O objetivo da EBNF não é adicionar novos recursos e sim conveniência, de forma que tudo que pode ser especificado em EBNF também pode ser especificado em BNF. A figura 6 utiliza a notação EBNF para representar as produções da gramática. O seguinte padrão é utilizado: • [...] ou (...)? implica 0 ou 1 ocorrência de qualquer construção dentro dos parênteses/chaves. • (...)+ implica 1 ou mais ocorrências de qualquer construção dentro dos parênteses. • (...)* implica 0 ou mais ocorrências de qualquer construção dentro dos parênteses. As strings entre os sı́mbolos “h” e “i” denotam os sı́mbolos terminais da gramática, já os trechos entre aspas compõem tokens da linguagem. A figura 6 contém a especificação da gramática no formato EBNF, a versão para o JavaCC está no anexo A. 4.6 Árvore de Sintaxe Abstrata A gramática livre de contexto previamente listada é utilizada para verificar se um programa atende às regras da sintaxe Prolog. Além disso, o parser deve construir uma representação interna do programa; a essa representação interna em forma de árvore damos o nome de Árvore de Sintaxe Abstrata (Abstract Syntax Tree 34 Program Clause Head Body Goal Terms Term List Expression RelExpression MatExpression Atom Variable Number ::= ::= ::= ::= ::= ::= ::= ::= ::= ::= ::= (Clause)+ <EOF> Head (":-" Body)? "." Atom ("(" Terms ")")? Goal ("," Goal)* ((Atom ( "(" Terms ")" )? ) | Expression | <CUTOPERATOR>) Term ("," Term )* (Atom ("(" Terms ")" )* | List | Variable | Number) "[" (Terms ("|" Term)?)? "]" Variable (RelExpression | MatExpression) <RELOPERATOR> (Variable | Number) <ISOPERATOR> (Variable | Number) (<ADDOPERATOR> (Variable | Number))? ::= <ATOM> ::= <VARIABLE> ::= <NUMBER> FIGURA 6: Gramática Prolog - AST). A fase seguinte do tradutor, o gerador de código, irá trabalhar com esta estrutura para a construção do programa Java. O estilo orientado a objetos de construir a árvore se dá com a criação uma classe para cada sı́mbolo não-terminal da gramática, isto é feito automaticamente pela ferramenta JJTree, a figura 7 ilustra graficamente a árvore gerada para a cláusula familia(X,Y) :- pai(X,Z),pai(Z,Y). 35 Program Clause Head Atom (familia) Body Terms Goal Term Term Variable (X) Variable (Y) Atom (pai) Goal Terms Atom (pai) Terms Term Term Term Term Variable (X) Variable (Z) Variable (Z) Variable (Y) FIGURA 7: Exemplo de Árvore de Sintaxe Abstrata 36 5 5.1 IMPLEMENTAÇÃO Ambiente de Execução do Prolog Café O run-time environment do Prolog Café consiste em um conjunto de classes Java que dão suporte à execução do código traduzido. A classe principal, Prolog.java, implementa entre outras coisas os registradores, a área de dados e a rotina de unificação. Existem algumas outras classes que trabalham em conjunto, provendo, por exemplo, estruturas para o gerenciamento do backtracking. É interessante observar que o mecanismo de funcionamento de tais classes é fortemente inspirado na WAM, provando, mais uma vez, a influência da mesma em todas as implementações Prolog subseqüentes a ela. Para cada tipo de termo Prolog, existe uma classe Java equivalente, a qual é uma sub-classe da classe abstrata Term: Classe VariableTerm Descrição Cria um objeto representando uma variável lógica. IntegerTerm Representa os valores do tipo inteiro. SymbolTerm Esta classe cria objetos representando átomos. ListTerm Modela as listas Prolog. StructureTerm Cria um objeto representando um termo composto, um termo composto consiste em um functor e uma seqüência de um ou mais termos chamados argumentos TABELA 3: Classes Java que modelam os termos Prolog De acordo com BANBARA (1997), a definição da classe abstrata Term torna fácil a manipulação em Java de vetores de termos, uma vez que, em um vetor do tipo Term, pode ser colocado qualquer objeto que seja uma sub-classe dele. 37 5.2 Indexação de Cláusulas Uma das técnicas de otimização utilizadas pelo Prolog Café, e que teve seu comportamento mantido no PROJAVA é conhecida como indexação de cláusulas (clause indexing); seu objetivo é reduzir o número de cláusulas para serem testadas e evitar a criação de pontos de escolha, sempre que possı́vel. Os resultados são execução mais eficiente e menor consumo de memória. A otimização mais trivial realizada por qualquer sistema Prolog é tentar apenas as cláusulas de um procedimento especı́fico ao invés de testar todas as cláusulas do programa, durante a busca por alguma cláusula unificante. A indexação de cláusulas é um pouco mais complexa: apenas as cláusulas em que o primeiro argumento unifica com o objetivo são selecionadas. Todas as cláusulas do predicado append/3 a seguir tem como primeiro argumento uma lista Prolog, através da técnica de indexação de cláusulas, em uma consulta do tipo append(teste, teste, teste). Como o primeiro argumento da consulta é um átomo, o resultado seria falha, sem mesmo precisar testar sequer uma única cláusula do predicado. append([],L,L). append([H|L],L1,[H|R]) :append(L,L1,R). 5.3 Funcionamento O nome do programa Prolog a ser traduzido é passado via linha de comando ao tradutor, entra então em ação o parser. Caso o programa atenda às regras léxicas e sintáticas da gramática Prolog e nenhum erro ocorra, o processo criará uma representação interna em forma de árvore: a figura 8 ilustra graficamente os módulos internos da ferramenta. A tarefa agora cabe ao gerador de código, o primeiro passo é percorrer a árvore em busca de informações úteis como o nome e aridade dos predicados e o número de cláusulas de cada predicado. Para cada predicado, também é analisado o tipo do primeiro argumento de cada cláusula, informações estas rele- 38 vantes para o recurso de clause indexing. Colhidos estes primeiros dados sobre o programa, começa a gravação das classes Java em disco, um predicado por vez, é gerada a classe base, que representa o ponto de entrada e então as classes adicionais para clada cláusula. A operação consiste em percorrer os ramos da árvore de sintaxe abstrata correspondentes a cada cláusula e gerar o código Java equivalente. A figura 9 mostra o processo completo, desde a leitura do arquivo Prolog até obtenção do bytecode Java. A execução do código traduzido segue o mesmo modelo demonstrado na figura 4 no capı́tulo 3. Código fonte Prolog Tradutor PROJAVA Análise Léxica Análise Sintática Geração da Árvore de Sintaxe Abstrata Geração do Código Java Código fonte Java FIGURA 8: Funcionamento Interno do PROJAVA 39 Código fonte Prolog Tradutor PROJAVA Código fonte Java Compilador javac Java bytecode FIGURA 9: Diagrama de Tradução de Prolog para Java 5.4 Estrutura do Código Traduzido A estrutura do código traduzido segue o mesmo modelo do Prolog Café, para cada predicado é criado um arquivo .java contendo uma classe pública, que consiste no ponto de entrada do predicado, e uma classe adicional privada, para cada cláusula do mesmo. Tomando como exemplo o predicado pai/2 a seguir, é criado um arquivo com o nome PRED pai 2.java. pai(leonardo, augusto). pai(leonardo, alberto). A classe pública, representando o entry point, é uma sub-classe de Predicate, o seu método construtor recebe os argumentos passados ao predicado e os coloca 40 em variáveis de instância (arg1, arg2); o método exec() é responsável pela execução, ele coloca os argumentos nos registradores da engine, seta o argumento de continuação e então faz uma chamada ao método call(). Este, por sua vez, cuida da chamada as cláusulas do predicado através de uma construção herdada da WAM (switch on term()) que implementa a indexação de cláusulas. public class PRED_pai_2 extends Predicate { static Predicate fail_0 = new PRED_fail_0(); static Predicate pai_2_1 = new PRED_pai_2_1(); static Predicate pai_2_2 = new PRED_pai_2_2(); static Predicate pai_2_var = new PRED_pai_2_var(); public Term arg1, arg2; public PRED_pai_2(Term a1, Term a2, Predicate cont) { arg1 = a1; arg2 = a2; this.cont = cont; } public PRED_pai_2(){} public void setArgument(Term[] args, Predicate cont) { arg1 = args[0]; arg2 = args[1]; this.cont = cont; } public Predicate exec() { engine.aregs[1] = arg1; engine.aregs[2] = arg2; 41 engine.cont = cont; return call(); } public Predicate call() { engine.setB0(); return engine.switch_on_term( pai_2_var, fail_0, pai_2_var, fail_0, fail_0 ); } public int arity() { return 2; } public String toString() { return "pai(" + arg1 + ", " + arg2 + ")"; } } Para cada cláusula é criada uma sub-classe da classe “base” do predicado, sendo o método exec() o responsável pelo código da cláusula. O primeiro passo então é a obtenção dos argumentos que estão nos registradores do ambiente de execução através da desreferência. A desreferência é necessária, porque caso o conteúdo do registrador seja uma variável, esta pode estar ligada a outra variável, e assim sucessivamente, de forma que é preciso seguir a trilha de ponteiros de uma variável lógica até encontrar o “último da lista”. Também é setado o parâmetro de continuação, que indica o próximo predicado a ser executado em caso de sucesso. 42 O código seguinte dentro do método exec() tenta fazer a unificação dos argumentos com o termos da cláusula, a engine provê o método unify() que implementa o algoritmo de unificação. Caso algum termo não unifique, é chamado o método engine.fail(), que irá se encarregar de tomar ação certa no caso de falha. final class PRED_pai_2_1 extends PRED_pai_2 { static SymbolTerm s1 = SymbolTerm.makeSymbol("leonardo"); static SymbolTerm s2 = SymbolTerm.makeSymbol("augusto"); public Predicate exec() { Term a1, a2; a1 = engine.aregs[1].dereference(); a2 = engine.aregs[2].dereference(); this.cont = engine.cont; if ( !s1.unify(a1, engine.trail) ) return engine.fail(); if ( !s2.unify(a2, engine.trail) ) return engine.fail(); return cont; } } 5.5 Benchmarks O teste de desempenho compara o tempo que os tradutores levam para converter o código Prolog em código Java. O desempenho de execução dos programas traduzidos não foi alvo de benchmarks, posto que o código gerado pelos dois tradutores é muito semelhante. Foi possı́vel constatar que o tradutor PROJAVA é em média dez vezes mais rápido no processo de tradução, contudo, vale observar que, na realidade, é o Prolog Café que apresenta um desempenho muito baixo, por ser um programa relativamente 43 Programa familia.pl fibo.pl listas.pl hanoi.pl No de linhas 15 9 23 11 Prolog Café 5.406s 4.753s 5.446s 5.443s PROJAVA Speedup 0.515s 10.50 0.499s 9.53 0.530s 10.28 0.523s 10.41 TABELA 4: Benchmarks grande em Prolog que foi traduzido para Java e possui todo overhead de emulação Prolog em Java. 5.6 Limitações Algumas caracterı́sticas do tradutor que faz parte do software Prolog Café ainda não foram completamente implementadas no PROJAVA, entre elas: o suporte completo a termos compostos aninhados e expressões, também aninhadas. O tratamento de tais recursos é de natureza altamente recursiva e, o suporte completo a eles será incorporado a versões futuras do PROJAVA. 44 6 CONCLUSÕES Enquanto o Holoparadigma não contar com um compilador e uma máquina virtual multiparadigma capazes de tratar os diferentes paradigmas suportados da forma mais otimizada possı́vel, a abordagem de tradução para Java é a alternativa mais viável. Entretanto, a ferramenta responsável pela conversão das ações modulares lógicas da Hololinguagem para Java sofria de sérios problemas de desempenho relativos ao tempo de tradução. Através de constações empı́ricas foi possı́vel verificar que: • o baixo desempenho devia-se ao fato de o tradutor ter sido escrito em Prolog, e então ser auto-traduzido para obtenção de uma versão do mesmo em Java; • o modelo de código gerado é fruto de muitos anos de pesquisa primeiramente na tradução de Prolog para C, e então de Prolog para Java, de forma que este modelo poderia permanecer sem modificações. Dessa forma, o trabalho consistiu em implementar um novo tradutor capaz de gerar código compatı́vel com o que é gerado pelo Prolog Café, afim de que se pudesse fazer o aproveitamento das classes Java que dão suporte à execução do código traduzido. Para isso, foi necessário o estudo de toda a teoria que envolve a construção de um software de tal natureza. A ferramenta JavaCC foi utilizada para geração do front-end do tradutor, enquanto o gerador de código foi implementado manualmente. Através de benchmarks, foi possı́vel verificar que este novo tradutor é cerca de 10 vezes mais rápido que o conversor do Prolog Café. Trabalhos futuros: • Aperfeiçoamento da ferramenta PROJAVA para suportar uma gama maior de recursos da linguagem Prolog, como por exemplo, suporte completo a termos compostos aninhados; • Uma das principais motivações do Holoparadigma é a exploração do paralismo implı́cito, fato que não está sendo realizado para o paradigma lógico. 45 Futuras versões do PROJAVA poderiam gerar código Java, que se utilizaria de múltiplas linhas de execução (threads) para exploração do paralelismo; • Conhecimento na realização do trabalho pode servir de base para o suporte do paradigma da programação em lógica, em uma futura máquina virtual multiparadigma. 46 7 REFERÊNCIAS BIBLIOGRÁFICAS AHO, Alfred V.; SETHI, Ravi; ULLMAN, Jefrrey D. Compiladores: Princı́pios, Técnicas e Ferramentas. Rio de Janeiro: LTC Editora, 1995. 344p. AÏT-KACI, Hassan. Warren’s Abstract Machine: A Tutorial Reconstruction. The MIT Press (out of print), 1991. Disponı́vel em http://isg.sfu.ca/~hak APPEL, A. W. Compiling with Continuations. Cambridge Univeristy Press, 1992. BANBARA, M.; TAMURA N. Java implementation of a linear logic programming language. In proceedings of Post-JICSLP’98 Workshop on Parallelism and Implementation Technology for Logic Programming Languages. 1997. BANBARA, M.; TAMURA, N. Translating a Linear Logic Programming Language into Java. Workshop on Parallelism and Implementation Technology (Constraint) Logic Programming Languages, Las Cruces, p. 19-39, December. 1999. BARBOSA, Jorge L. V.; DU BOIS, André R.; FRANCO, Laerte K.; GEYER, Cláudio F. R. Traduzindo Holo para Java. 2001. BARBOSA, Jorge L. V.; Geyer, Cláudio F.R. Integrating Logic Blackboards and Multiple Paradigm for Distributed Software Development. Proceedings of Intr. Conference on Parallel and Dist. Processing Techniques and Applications (PDPTA). June. 2001. BARBOSA, Jorge L. V.; “Holoparadigma: um Modelo Multiparadigma Orientado ao Desenvolvimento de Software Distribuı́do”. Porto Alegre: CPGCC da UFRGS, 2002. 221p. (Teste, Doutorado em Ciência da Computação). 47 COLMERAUER A.; ROUSSEL, P. The birth of Prolog. Novembro. 1992. GRUNE, Dick; BAL, Henri E.; JACOBS, Ceriel J. H.; LANGENDOEN, Koen G. Projeto Moderno de Compiladores: Implementação e Aplicações. Rio de Janeiro: Campus, 2001. 671p. HORSPOOL, R. N.; LEVY, M. R. Translator-Based Multiparadigm Programming. JavaCC - The Java Parser Generator. http://www.webgain. com/products/metamata/java_doc.html, Dezembro. 2001. JJTree. http://www.webgain.com/products/java_cc/jjtree. html, Novembro, 2002. jProlog. http://www.cs.kuleuven.ac.be/~bmd/PrologInJava/, Novembro, 2002. KRALL, A. Implementation Techniques for Prolog. LINDGREN, Thomas. A Continuation-Passing Style for Prolog. Technical Report 86. Uppsala University. 1994. PALAZZO, L. A. M. Introdução à Programação PROLOG. Pelotas: EDUCAT, 1997. 353p. PANKAJ, G. The Design and Implementation of Prolog Parser Using JavaCC. University of North Texas. Maio de 2002. (Dissertação, Mestrado em Ciência da Computação) SETHI, Ravhi. Programming Languages: Concepts & Consructs. Murray Hill, New Jersey: Addison-Wesley, 1997. 640p. 2.ed. SCOWEN, R. S. Draft prolog standard, Technical Report ISO/IEC JTC1 SC22 WG17 N110, International Organization for Standardization, 1993. 48 STERLING, L; SHAPIRO, E. The Art of Prolog 2.ed. Cambridge: MIT Press, 1994. 509 p. TARAU, P.; BOYER, M. Elementary Logic Programs. Proceedings of Programming Language Implementation and Logic Programming, number 456 in Lecture Notes in Computer Science, páginas 159-173. Springer: Agosto. 1990. TARAU, P. Jinni: Intelligent Mobile Agent Programming at the Intersection of Java and Prolog. PAAM’9, The Practical Applications Company. 1999. WERNER, Otilia. Uma Máquina Abstrata Estendida para o Paralelismo E na Programação em Lógica. Porto Alegre: CPGCC da UFRGS, 1994. 150p. (Dissertação, Mestrado em Ciência da Computação). 49 A Gramática Prolog para o JavaCC options { MULTI = true; } PARSER_BEGIN(PrologParser) public class PrologParser { public static void main(String arguments[]) throws ParseException { } } PARSER_END(PrologParser) SPECIAL_TOKEN : /* Comments */ { <SINGLE_LINE_COMMENT1: "%" (~["\n","\r"])* ("\n"|"\r"|"\r\n")> | <SINGLE_LINE_COMMENT2: "//" (~["\n","\r"])* ("\n"|"\r"|"\r\n")> | <MULTI_LINE_COMMENT: "/*" (~["*"])* "*" (~["/"] (~["*"])* "*")* "/"> } SKIP: { " " | "\t" | "\r" | "\n" } TOKEN: /* Operators { < CUTOPERATOR : | < ADDOPERATOR : | < MULOPERATOR : | < RELOPERATOR : | < ISOPERATOR : | < MATOPERATOR : } */ "!" > "+" | "-" > "*" | "/" | "mod" > "=" | "==" | "!=" | ">" | "<" | ">=" | "<=" > "is" > <ADDOPERATOR> | <MULOPERATOR> > 50 TOKEN: /* Numeric constants */ { < NUMBER: (<DIGIT>)+ > | < #DIGIT: ["0" - "9"] > | < CHAR_LITERAL: "’" (~["’","\\","\n","\r"])* } "’" > TOKEN: /* Function names */ { < VARIABLE: ( (<HICASE> | "_" ) ( <ANYCHAR> )* ) > | < ATOM: ( <LOCASE> ) ( <ANYCHAR> )* > | < #ANYCHAR: ( <LOCASE> | <HICASE> | <DIGIT> | "_" ) > | < #LOCASE: ["a"-"z"] > | < #HICASE: ["A"-"Z"] > } ASTProgram Program() : {} { (Clause())+ <EOF> { return jjtThis; } } void Clause() : {} { Head() [ ":-" Body() ] "." } void Head() : {} { Atom() [ "(" Terms() ")" ] } void Body() : {} { Goal() ("," Goal())* } 51 void Goal() : {} { ((Atom() [ "(" Terms() ")" ]) | Expression() | <CUTOPERATOR>) } void Terms() : {} { Term() ("," Term())* } void Term() : {} { Atom() ("(" Terms() ")")* | List() | Variable() | Number() } void List() : {} { "[" [ Terms() ["|" Term()]] "]" } void Expression() : {} { Variable() (RelExpression() | MatExpression()) } void RelExpression() : { Token t; } { t = <RELOPERATOR> (Variable() | Number()) { jjtThis.setOp(t.image); } } void MatExpression() : 52 { Token t = null; } { <ISOPERATOR> (Variable() | Number()) [ t = <ADDOPERATOR> (Variable() | Number()) ] { jjtThis.setOp(t.image); } } void Atom() : { Token t; } { t = <ATOM> { jjtThis.setName(t.image); } } void Variable() : { Token t; } { t = <VARIABLE> { jjtThis.setName(t.image); } } void Number() : { Token t; } { t = <NUMBER> { jjtThis.setName(t.image); } } 53 B Programas Prolog usados nos benchmarks B.1 Árvore Genealógica familia(X,Y) familia(X,Y) familia(X,Y) familia(X,Y) ::::- pai(X,Z),pai(Z,Y). pai(X,Z),mae(Z,Y). mae(X,Z),pai(Z,Y). mae(X,Z),mae(Z,Y). pai(nadir,jorge). pai(nadir,bia). pai(nadir,sergio). pai(sergio,antonio). pai(sergio,helena). mae(zita,jorge). mae(zita,bia). mae(zita,sergio). mae(bia,victoria). mae(bia,catarina). B.2 Série de Fibonacci fib(1,1). fib(2,1). fib(M,N) :M > 2, M1 is M-1, M2 is M-2, fib(M1,N1), fib(M2,N2), N is N1+N2. B.3 Manipulação de Listas listas(T,L,Z,K) :gera_lista(T,L), 54 nrev(L,Z), size(Z,K). nrev([],[]). nrev([H|L],R) :nrev(L,R1), append(R1,[H],R). append([],L,L). append([H|L],L1,[H|R]) :append(L,L1,R). gera_lista(0,[]). gera_lista(N,[Ca|Co]) :Ca is N - 1, gera_lista(Ca,Co). size([],0). size([X|Y],T) :size(Y,Z), T is Z + 1. B.4 Torres de Hanói hanoi(1,A,B,C,[mv(A,C)]). hanoi(N,A,B,C,M) :N > 1, N1 is N - 1, hanoi(N1,A,C,B,M1), hanoi(N1,B,A,C,M2), append(M1,[mv(A,C)],T), append(T,M2,M). append([],L,L). append([H|L],L1,[H|R]) :append(L,L1,R). 55 C Exemplo de Código Traduzido: Fibonacci // // // // Generated Java file by ProJava A Prolog to Java source-to-source translation system September 2002 Author: Augusto Mecking Caringi import jp.ac.kobe_u.cs.prolog.lang.*; public class PRED_fib_2 extends Predicate { static Predicate fail_0 = new PRED_fail_0(); static Predicate fib_2_1 = new PRED_fib_2_1(); static Predicate fib_2_2 = new PRED_fib_2_2(); static Predicate fib_2_3 = new PRED_fib_2_3(); static Predicate fib_2_var = new PRED_fib_2_var(); public Term arg1, arg2; public PRED_fib_2(Term a1, Term a2, Predicate cont) { arg1 = a1; arg2 = a2; this.cont = cont; } public PRED_fib_2(){} public void setArgument(Term[] args, Predicate cont) { arg1 = args[0]; arg2 = args[1]; this.cont = cont; } public Predicate exec() { engine.aregs[1] = arg1; engine.aregs[2] = arg2; engine.cont = cont; 56 return call(); } public Predicate call() { engine.setB0(); return engine.switch_on_term( fib_2_var, fib_2_var, fail_0, fail_0, fail_0 ); } public int arity() { return 2; } public String toString() { return "fib(" + arg1 + ", " + arg2 + ")"; } } final class PRED_fib_2_var extends PRED_fib_2 { static Predicate fib_2_var_1 = new PRED_fib_2_var_1(); public Predicate exec() { return engine.jtry(fib_2_1, fib_2_var_1); } } final class PRED_fib_2_var_1 extends PRED_fib_2 { static Predicate fib_2_var_2 = new PRED_fib_2_var_2(); public Predicate exec() { 57 return engine.retry(fib_2_2, fib_2_var_2); } } final class PRED_fib_2_var_2 extends PRED_fib_2 { public Predicate exec() { return engine.trust(fib_2_3); } } final class PRED_fib_2_1 extends PRED_fib_2 { static IntegerTerm s1 = new IntegerTerm(1); static IntegerTerm s2 = new IntegerTerm(1); public Predicate exec() { Term a1, a2; a1 = engine.aregs[1].dereference(); a2 = engine.aregs[2].dereference(); this.cont = engine.cont; if ( !s1.unify(a1, engine.trail) ) return engine.fail(); if ( !s2.unify(a2, engine.trail) ) return engine.fail(); return cont; } } final class PRED_fib_2_2 extends PRED_fib_2 { static IntegerTerm s1 = new IntegerTerm(2); static IntegerTerm s2 = new IntegerTerm(1); public Predicate exec() { Term a1, a2; a1 = engine.aregs[1].dereference(); a2 = engine.aregs[2].dereference(); this.cont = engine.cont; 58 if ( !s1.unify(a1, engine.trail) ) return engine.fail(); if ( !s2.unify(a2, engine.trail) ) return engine.fail(); return cont; } } final class PRED_fib_2_3 extends PRED_fib_2 { public Predicate exec() { Term a1, a2, a3, a4, a5, a6; a1 = engine.aregs[1].dereference(); a2 = engine.aregs[2].dereference(); this.cont = engine.cont; a3 = engine.makeVariable(); a4 = engine.makeVariable(); a5 = engine.makeVariable(); a6 = engine.makeVariable(); Predicate p1 = new PRED_$plus_3(a5, a6, a2, cont); Predicate p2 = new PRED_fib_2(a4, a6, p1); Predicate p3 = new PRED_fib_2(a3, a5, p2); Predicate p4 = new PRED_$minus_3(a1, new IntegerTerm(2), a4, p3); Predicate p5 = new PRED_$minus_3(a1, new IntegerTerm(1), a3, p4); return new PRED_$greater_than_2(a1, new IntegerTerm(2), p5); } } 59 D Exemplo de Holoprograma // datamining.holo //******************** ENTE PRINCIPAL ******************** holo() // Ente principal. { holo() // Acao guia. { writeln(’HOLO: Vou criar tres minas e um mineiro’), clone(mine(1),mine_d1), // Cria a primeira mina. clone(mine(2),mine_d2), // Cria a segunda mina. clone(mine(3),mine_d3), // Cria a terceira mina. clone(miner,miner_d), // Cria o mineiro. for X := 1 to 3 do // Aguarda pelos resultados da mineracao. { history#list(#Ident,#Num,#Fibo), // Obtem o resultado de uma mineracao. writeln(’HOLO: Terminou a mineracao da mina:’,Ident), writeln(’HOLO: Fibonacci de ’,Num,’ e ’,Fibo) } writeln(’HOLO: Terminou a mineracao’) } } //******************** mine() { mine(Ident) { writeln(’MINA } history { list(1,2). list(2,4). list(3,6). } } ENTE MINA ******************* ’,Ident,’: Fui criada’) // // // // A historia da mina de cada mina possui o mesmo conteudo. O primeiro numero identifica a mina. O segundo e o numero usado para o calculo do Fibonacci. 60 //******************** ENTE MINEIRO ******************* miner() { miner() { writeln(’MINEIRO: Inicio da mineracao.’), move(self,mine_d1), // Passo 1 - Entra na mina 1 mining(1,Num1,Res1), // Passo 2 - Minera a mina 1 move(self,out), // Passo 3 - Sai da mina 1 out(history)!list(1,Num1,Res1), // Passo 4 - Salva o resultado 1 move(self,mine_d2), // Passo 5 - Entra na mina 2 mining(2,Num2,Res2), // Passo 6 - Minera a mina 2 move(self,out), // Passo 7 - Sai da mina 2 out(history)!list(2,Num2,Res2), // Passo 8 - Salva o resultado 2 move(self,mine_d3), // Passo 9 - Entra na mina 3 mining(3,Num3,Res3), // Passo 10 - Minera a mina 3 move(self,out), // Passo 11 - Sai da mina 3 out(history)!list(3,Num3,Res3), // Passo 12 - Salva o resultado 3 writeln(’MINEIRO: Fim da mineracao.’) } mining(Ident,Num,Result) // IA que realiza a mineracao. { out(history)#list(Ident,#Num), // Minera a historia externa para // obtencao de um dado. fib(Num,#Result) // Chama a MLA para determinar Fibonacci. } fib/2() // Acao Modular Logica (MLA) que calcula Fibonacci. { fib(1,1). fib(2,1). fib(M,N) :M > 2, M1 is M-1, M2 is M-2, fib(M1,N1), fib(M2,N2), N is N1+N2. } }