AplicaÇÃo de GramÁticas Formais e AutÔmatos Para a DefiniÇÃo de
Linguagens de ProgramaÇÃo
1. IntroduÇÃo
Os conceitos de gramáticas formais e autômatos encontram uma aplicação natural na definição de Linguagens de Programação (LP). Definições formais precisas e completas da semântica e sintaxe de Linguagens de Programação tem sido um objetivo constante ao longo dos anos para a Ciência da Computação.
Uma dúvida surge naturalmente: porque tanta preocupação em definir formalmente LP ? A resposta também é natural e imediata: uma definição formal precisa de LP é útil tanto para o implementador da linguagem quanto para o programador. Para o implementador, a definição formal fornece uma especificação precisa do que exatamente precisa ser implementado. Para o programador, a definição fornece uma base sólida para a geração do código, ou seja, algumas questões tais como estrutura sintática ou de significado podem ser resolvidas simplesmente tendo como referência a definição formal, evitando que erros sejam cometidos na geração do código.
Neste documento será descrita uma técnica para definição formal de Linguagens de Programação. A definição consiste de duas partes. Na Primeira parte, uma gramática formal, chamada gramática par é usada para traduzir o código fonte de um programa para uma representação em estrutura de grafos direcionados. O resultado da tradução é um conjunto de grafos direcionados que representam o estado inicial de um autômato. O autômato se move através de uma seqüência de estados (cada um representado como um conjunto de grafos direcionados) que correspondem à seqüência de execução do programa original. A principal diferença entre os autômatos aqui descritos e os modelos usuais de autômatos está no uso de um estado altamente estruturado e uma função de transição complexa correspondente para mudar de estado para estado.
Na segunda parte, um autômato executa o programa traduzido anteriormente. Esta parte é considerada uma abstração direta
da estrutura comum da implementação de uma LP. Na implementação usual de uma linguagem, o
compilador (ou tradutor) traduz o código fonte de entrada do programa para um programa
executável, em linguagem de máquina ou em alguma forma intermediária. O programa traduzido é então executado
diretamente ou pelo hardware interpretador de linguagens de máquinas ou por um software
programado como interpretador.
Muitas outras técnicas de
definição de linguagens de programação foram sugeridas baseadas neste paradigma
tradutor-autômato. A linguagem VDL ( Vienna Definition Language ), desenvolvida pelo IBM Vienna Laboratory, é a mais conhecida. Através da VDL, uma definição formal de LP toma a forma de um tradutor e um autômato. O tradutor define um mapeamento entre programas
representados por strings de caracteres e programas representados por árvores. O autômato que executa os programas em forma de árvores é
definido em termos de:
1. Um estado inicial;
2. Uma regra de transição entre os
estados;
3. Um conjunto de instruções primitivas
(transformações sobre a árvore).
O autômato se move por uma seqüência de estados (cada estado representado por uma árvore) através da aplicação de uma regra de transição (2) no estado em que se encontra. Esta regra determina uma instrução primitiva (3) a ser executada para transformar o atual estado (árvore) no próximo estado do autômato representado por outra árvore.
A técnica de definição formal de linguagens de programação descrita neste documento, difere desta anterior pelo fato de utilizar grafos hierárquicos direcionados (H-grafos) para representar os programas e os estados do autômato, e também por utilizar definições de gramáticas formais chamadas gramáticas pares do tradutor.
A definição de gramáticas pares de tradutores, fornece definições simples do mapeamento entre um programa e sua representação associada a um grafo. Gramáticas pares são construídas à partir de duas gramáticas livres de contexto, uma gramática de "string" e uma gramática de "grafos". Muitos dos conceitos aplicados relacionados à gramáticas livre de contexto são também aplicados a gramáticas pares.
Neste documento serão definidas três estruturas básicas :
1. Grafos direcionados e H-grafos;
2. Gramáticas pares;
3. Autômatos de H-grafos
2.1 Grafos HierÁrquicos (H-Grafos)
Grafo hierárquico ou H-grafo, é um conjunto finito de grafos direcionados sobre um conjunto comum de nós, organizados em uma hierarquia. Assumimos:
· N (nodes) um conjunto de nós;
· A (atomic) um conjunto de dados básicos atômicos;
· L (label) um conjunto de arcos rotulados.
Def 2.1.1 : Um grafo direcionado estendido G sobre N e L é uma tripla G= ( M, E, S ), onde:
· M é um subconjunto finito não vazio de N;
· E (é o conjunto de arcos de G) é um conjunto finito da forma ( n, a, m );
· S é o nó inicial
onde n, m Î M e a ÎL, SÎM. Se ( n, a, m ) Î E, pode-se dizer que existe um arco rotulado 'a' do nó 'n' ao nó 'm' de G.
Def 2.1.1 : Um H-grafo H sobre N, A, e L é um par H= (M, V) , onde:
· M é um subconjunto de N, não vazio e finito;
· V é uma função de valor e conteúdo que mapeia M para A È {G' | G' é um grafo direcionado estendido sobre M e L}.
· Assim, se 'n' é um nó de M, então V(n) é designado valor ou conteúdo de 'n'.
H-grafos são estruturas formais usadas para modelar programas e estruturas de dados. A figura 1 mostra um exemplo de um grafo direcionado estendido. Na definição de LP a saída do tradutor (que também é o estado inicial do autômato) é um H-grafo, assim como são cada um dos demais estados através dos quais o autômato passa durante a execução do programa.
Os H-grafos representam melhor uma nova ordem da hierarquia dos grafos. Começando de qualquer nó de um H-grafo, podemos descer um nível, aplicando a função de valor V. Após a aplicação de V, encontraremos um nó, que pode vir a ser um valor atômico em A ou um grafo. Desta forma um H-grafo é organizado hierarquicamente por níveis. Descendo um nível do grafo, nós podemos encontrar nós que já apareceram anteriormente em níveis mais altos do grafo. De fato esta hierarquia pode ser recursiva, ou seja, um nó recém alcançado no grafo pode ser um nó pelo qual nós descendemos de um nível mais alto.
Este aspecto da definição do H-grafo permite uma modelagem natural de muitos construtores de linguagens de programação.
2.2 GramÁticas Pares
Uma gramática par é composta de um par de gramáticas formais cujas produções e símbolos não terminais estão relacionados um a um. As duas gramáticas são chamadas de gramática à esquerda e gramática à direita, respectivamente. Uma gramática par ou é uma gramática de string livre de contexto ou é uma gramática de grafo livre de contexto.
Uma gramática par G é uma quádrupla G= ( K, T, S, P ), onde:
· K é um conjunto finito de símbolos não terminais
· T é conjunto finito de símbolos terminais
· S Î K e é o símbolo inicial
· P é um conjunto de produções
Cada produção de P é composta de um par de produções de gramáticas livre de contexto de grafo ou string. As produções são respectivamente uma à esquerda outra à direita. O conjunto de todas as produções à esquerda de G define uma gramática livre de contexto, denominada gramática à esquerda de G. Logo, esta gramática à esquerda de G define uma linguagem à esquerda de G. A gramática à direita e a linguagem à direita de G são definidas com um raciocínio similar.
A linguagem L( G ) definida por uma gramática G consiste de um conjunto de pares ordenados de elementos das linguagens à direita e à esquerda, respectivamente de G. Para gerar um par em L ( G ), começamos com o par ( S, S ), que consiste do símbolo inicial de G nas posições à esquerda e à direita. Um produção ( S®a, S®a) de G é escolhida. A aplicação desta produção para o par ( S,S ), produz um novo par (a, a) onde a e a contém os mesmos símbolos não terminais em correspondência 1-1. Derivando desta maneira nós chegaremos a um par terminal ( w, w) contendo símbolos não terminais, ( w, w) é então um elemento de L ( G ), com w na linguagem à esquerda e w na linguagem à direita de G.
Para a definição de uma linguagem de programação o par ( w, w) na linguagem definida por uma gramática par, w deve ser considerado como um programa de entrada e w a sua representação gráfica. Logo, a gramática par G define a translação entre programas e sua representação gráfica correspondente.
2.2.1 GramÁticas de Grafos
Para a definição de linguagem de programação usando H-grafos nós desejamos que uma gramática à direita de um par defina uma linguagem composta de H-grafos , enquanto que para a gramática à esquerda, a linguagem definida deve ser um conjunto de strings de caracteres representando programas sintaticamente corretos na linguagem. Uma gramática livre de contexto normal é suficiente para a gramática à esquerda. Porém a gramática à direita requer uma extensão do conceito de uma gramática livre de contexto normal para o de uma gramática livre de contexto de grafos, que é uma gramática livre de contexto que define uma linguagem composta de um conjunto de H-grafos
Uma gramática livre de contexto F é uma quádrupla F = ( K, A, S, R ) onde:
· K é um conjunto finito de não terminais
· A é um conjunto de terminais
· S Î K é o símbolo inicial
· R é um conjunto finito de produções
Cada produção em R é uma quíntupla ( C, H, G, I, O ) onde C Î K ( o não terminal à esquerda ) e H, G, I e O formam o lado direito da produção. H = ( M, V ) é um H-grafo com átomos escolhidos de um conjunto K È A, G é um grafo direcionado estendido sobre M e A, com nó inicial I. O, o nó final, é também um nó em G.
Uma gramática livre de contexto de grafos funciona praticamente da mesma maneira que uma gramática livre de contexto normal para gerar um grafo em sua linguagem. Você começa com um nó que contém como seu valor o símbolo de início da gramática. Cada regra da gramática de grafos especifica um símbolo não terminal à esquerda e um H-grafo à direita. Estes grafos à direita podem conter outros nós com valores não terminais. Quando todos estes nós forem substituídos por nós terminais, o grafo resultante é uma linguagem de grafo definida pela gramática. A única dificuldade a mais no processo de geração vem da necessidade de especificar como ligar o grafo a ser substituído por um grafo já existente quando um nó não terminal é substituído por um grafo de com uma regra da gramática. Em gramáticas de grafos simples, isto é feito simplesmente especificando um nó inicial e um nó final no grafo à direita de cada regra da gramática. Quando um nó não terminal é substituído por um grafo, cada arco chegando neste nó não terminal é conectado ao nó inicial de um novo grafo, e cada arco saindo de um nó não terminal é conectado ao nó final de um novo grafo.
A figura 2 esclarece os conceitos de gramáticas pares e grafos de gramáticas e também serve para introduzir a notação. Considerando as produções do while e expressão condicional acima, as produções à esquerda e à direita são apresentadas sob a seguinte notação: não terminais são palavras inclusas em < . . . > e o símbolo ::= separa duas partes de uma produção ( necessário devido ao uso de ® em H-grafos). Nós não terminais têm o formato oval, enquanto nós terminais são representados por retângulos. Os nós inicial e finais em cada produção de gramáticas de grafo são marcados por * e **, respectivamente.
2.2.2.
NÓs Rotulados e H-Grafos Reduzidos
Uma simples técnica nos permite fazer as conecções necessárias. Nós permitimos que os nós terminais nos H-grafos gerados por uma gramática de grafos sejam rotulados com um identificador. Tipicamente muitos nós em um H-grafo gerado por uma gramática par terá o mesmo rótulo. No H-grafo terminal da tradução de um programa, todos os nós com o mesmo rótulo são unidos em um único nó, deixando assim todos os arcos que eram ligados a qualquer um dos nós originais, ligados a um novo nó composto resultante. É atribuído a este nó composto o valor nulo # ou qualquer outro valor não nulo.
A regra de unir todos os nós com rótulos idênticos é chamada de regra de redução básica e os H-grafos resultantes são chamados de H-grafos reduzidos. Muitas vezes unindo esses nós tornamos o nó resultante em um nó global.
Os H-grafos reduzidos resultantes do tradutor de uma Gramática Par representam programas executáveis. A execução é definida em termos de um autômato H-grafo capaz de seguir um caminho através do grafo de um programa e executar as operações encontradas ao longo do caminho. Ordinariamente a figura de um autômato aparece em nossa mente como uma fita, uma posição inicial e estado interno, junto com uma função de transição que define como um autômato deve mover através de uma seqüência de estados, onde cada estado consiste de uma tripla: conteúdo da fita, posição inicial e estado interno. No nosso autômato H-grafo, cada estado é representado por um H-grafo. O autômato H-grafo começa em um estado inicial que consiste de um H-grafo reduzido como a saída de uma Gramática Par. O autômato move através de uma seqüência de estados, cada um representando a execução de uma operação primitiva do programa original. Em último lugar o autômato pode parar. Se isto acontecer o estado final representa o resultado da execução do programa original.
Def 2.3.1 : Um autômato H-grafo B é um quíntupla B = ( N, A, S, S0, d ), onde:
· N é o universo de nós
· A é um conjunto de átomos
· S é um conjunto de estados ( um conjunto de H-grafos sobre N e A )
· S0 Í S é o conjunto de estados iniciais
· d: S ® S é a função de transição
d é definida em termos de:
(1) uma instrução-buscar-e-executar (um ciclo de dois passos)
(2) um conjunto de esquemas de instruções primitivas, cada um definido como uma transformação de H-grafo estritamente local.
Com este conhecimento de H-grafos, Gramáticas Pares e Autômatos de H-grafos, temos uma visão mais nítida das linguagens de programação.
Series in Automatic Computation. Prentice-Hall. 1976