A Implementação de Processadores de
Linguagens
Análise Léxica, Sintática e Semântica
Ferramentas em Prolog, Pascal, C++ e Java
Eloi L. Favero
Departamento de Informática
CCEN - UFPA
2002
[email protected]
ii
• Parte I: Fundamentos e Técnicas de Programação de Gramáticas
–
–
–
–
Conceitos de Linguagens Formais
Gramáticas Regulares e Automatos
Gramaticas Livres de Contexto
Gramaticas de Atributos
∗ Léxico : Regulares e Automatos
– Programação de Gramáticas: ∗ Sintático: Livres de Contexto
∗ Semântico: Gramática de Atributos
• Parte II:Estudos de Casos e Aplicações
– Programação de Gramáticas em :
Prolog, Pascal, C, C++, Java
– Ferramentas para Processadores de Linguagens
– Programação de Compiladores
– Processamento de Linguagem Natural:
Léxico/Morfologia, Sintaxe, Geração de Linguagem Natural
iii
Eloi L. Favero
Departamento de Informática
[email protected]
Copyright c 2002
2002
Dedicatória
Para Flori, Emmanuel, Ayun e Thiago.
i
Sumário
Dedicatória
i
1 Fundamentos de Linguagens Formais
1.1 Nı́veis lingüı́sticos . . . . . . . . . . . . .
1.2 Notações gramaticais:BNF (Backus Naur
1.3 Hierarquia de Chomsky . . . . . . . . . .
1.3.1 Sem restrições . . . . . . . . . . .
1.3.2 Sensı́vel ao contexto . . . . . . .
1.3.3 Livre de contexto . . . . . . . . .
1.3.4 Regular . . . . . . . . . . . . . .
1.4 Mais sobre classificação de linguagens . .
1.5 Gramáticas em Prolog: DCG . . . . . .
1.5.1 Gramática regular . . . . . . . .
1.5.2 Gramática livre de contexto . . .
1.5.3 Gramática sensı́vel ao contexto .
1.6 Exercı́cios avançados . . . . . . . . . . .
. . . .
Form)
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
. . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2 Fundamentos para GRs, GLCs e GAs
2.1 Gramáticas Regulares e Autômatos . . . . . . . . . . . . . .
2.1.1 Transformação de expressão regular para autômato .
2.1.2 Transformação de gramática regular para autômato .
2.1.3 Transformado uma expressão regular numa gramática
2.1.4 Removendo não determinismo, com fatoração . . . .
2.2 Gramáticas Livres de Contexto (GLC) . . . . . . . . . . . .
2.2.1 Análise ascendente LR(k) e descendente LL(k) . . . .
2.2.2 Recursividade à esquerda ou à direita . . . . . . . . .
2.2.3 Fatoração . . . . . . . . . . . . . . . . . . . . . . . .
2.2.4 Análise sintática descendente . . . . . . . . . . . . .
2.2.5 Análise sintática ascendente . . . . . . . . . . . . . .
2.3 Gramáticas de Atributos . . . . . . . . . . . . . . . . . . . .
2.4 Calculando o valor de um número binário . . . . . . . . . . .
2.5 Avaliar expressões aritméticas . . . . . . . . . . . . . . . . .
2.5.1 Programando a GLC como DCG . . . . . . . . . . .
2.5.2 Calculando o valor com equações semânticas . . . . .
ii
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
2
4
6
7
7
8
8
10
12
13
14
15
16
. . . . .
. . . . .
. . . . .
regular
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
. . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
17
18
18
19
21
25
26
27
28
30
31
33
35
40
43
44
45
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
iii
SUMÁRIO
2.5.3 O problema da associatividade à esquerda para LL(k) . . . . . . .
2.5.4 Gerando notação polonesa com ações semânticas . . . . . . . . . .
2.6 Regras gramaticais reversı́veis: geração x reconhecimento . . . . . . . . .
3 Técnicas para Programação de Gramáticas
3.1 Medidas de tempo . . . . . . . . . . . . . . . .
3.2 Programação de gramáticas regulares . . . . . .
3.3 Programação de gramáticas livres de contexto .
3.4 Programação de Gramáticas de Atributos (GAs)
3.4.1 Método da costura com atributos . . . .
3.4.2 Exercı́cios de Revisão . . . . . . . . . . .
46
48
50
.
.
.
.
.
.
53
54
56
60
65
66
67
.
.
.
.
.
.
.
68
69
69
72
74
74
79
84
.
.
.
.
.
.
.
.
87
87
87
90
92
93
94
95
96
6 Programação de Gramáticas Livres de Contexto
6.1 Versão em Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.2 Versão em C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.3 Versão em Pascal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
99
99
101
103
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
4 Programação de Gramáticas em Prolog
4.1 Análise sintática e semântica . . . . . . . . . . . . . . . .
4.1.1 Calcular expressões aritméticas com variáveis . .
4.1.2 Traduzir SQL para álgebra relacional . . . . . . .
4.2 Análise léxica e Autômatos . . . . . . . . . . . . . . . . .
4.2.1 DCGs para análise léxica . . . . . . . . . . . . . .
4.2.2 Autômatos trabalhando com arquivos . . . . . . .
4.2.3 Gerando palavras reservadas e números de linhas
5 Programação de autômatos
5.1 Métodos de codificação de reconhecedores
5.1.1 Versão em C++ . . . . . . . . . . .
5.1.2 Versão em Pascal . . . . . . . . . .
5.1.3 Versão em Java . . . . . . . . . . .
5.2 Contagem de tempo . . . . . . . . . . . .
5.2.1 Versão em C++ . . . . . . . . . . .
5.2.2 Versão em Pascal . . . . . . . . . .
5.2.3 Versão em Java . . . . . . . . . . .
7
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
Programação de Gramáticas de Atributos
106
7.1 Versão em Pascal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
7.2 Versão em C(C++) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
7.3 Versão em Java . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
8 Exerı́cios e Projetos de Programação
111
8.1 Programação de Gramáticas . . . . . . . . . . . . . . . . . . . . . . . . . 111
8.2 Integrando Léxico e Sintático . . . . . . . . . . . . . . . . . . . . . . . . 113
8.3 Gramática fatorada: sem retrocesso . . . . . . . . . . . . . . . . . . . . . 114
SUMÁRIO
8.4
Gramática não fatorada: método da costura . . . . . . . . . . . . . . . .
8.4.1 Calcular expressões aritméticas com variáveis . . . . . . . . . . .
iv
115
117
Lista de Figuras
2.1
2.9
Autômato finito correspondente a a expressão regular a*b*; obtido pelo
algoritmo de Thompson. . . . . . . . . . . . . . . . . . . . . . . . . . . .
Autômato finito não determinı́stico, obtido pelo método de Thompson, a
partir da expressão regular (a|b)*abb. . . . . . . . . . . . . . . . . . .
Autômato finito não determinı́stico para a expressão regular (a|b)*abb,
sem as transições vazias . . . . . . . . . . . . . . . . . . . . . . . . . . .
Autômato finito determinı́stico associado a versão GLUD da gramática sr.
Autômato finito determinı́stico, para a expressão regular (a|b)*abb, obtido pelo método de subconjuntos de estados alcançáveis, a partir da versão
não determinı́stica do autômato, da Figura 2.3. . . . . . . . . . . . . . .
Autômato finito para valores inteiros; versões: não determinı́stica e determinı́stica. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Árvore de uma sentença ”aaa”, para uma gramática S-GA, com atributos
só sintetizados (sobem) para contar os (a)s. . . . . . . . . . . . . . . . .
Árvore para a sentença ”bbb”, da gramática L-GA, com atributos herdados
(descem) e sintetizados (sobem), contando os (b)s. . . . . . . . . . . . . .
Árvore com atributos herdados (descem) e sintetizados (sobem). . . . . .
38
41
4.1
4.2
Autômato finito para tokens de expressões aritméticas. . . . . . . . . . .
Integração entre os componentes Léxico e Sintático. . . . . . . . . . . .
75
80
8.1
Integração entre os componentes Léxico e Sintático.
2.2
2.3
2.4
2.5
2.6
2.7
2.8
v
. . . . . . . . . . .
19
20
20
22
24
25
37
113
Capı́tulo 1
Fundamentos de Linguagens Formais
Prolog (1972) Definite Clause Grammar - DCG
Kowalski, Colmerauer, ... Pereira
Bakus e Naur (1960) – Bakus Naur Form
Chomski (1956, 1959)
Gramáticas regulares
Neste capı́tulo revisamos os principais conceitos de Linguagens Formais. Apresentamos uma classificação das gramáticas baseada na hierarquia de Chomsky e também
uma outra classificação mais simples baseada nos nı́veis lingüı́sticos: léxico, sintático e
semântico. Mostramos como utilizar o formalismo gramatical embutido no Prolog, DCG,
para programar os diferentes tipos de gramáticas.
A disciplina de Linguagens Formais nasceu na metade dos anos 50 a partir de estudos
para descrição de linguagens naturais. Porém, tivemos grandes avanços nesta disciplina
devido ao uso destes mesmos formalismos para especificação de linguagens artificiais (de
programação) já no final da década de 50, quando foi especificado o ALGOL 60, usando-se
a notação BNF de gramáticas livres de contexto.
Aqui são enunciados vários resultados de Linguagens Formais mas não são mostradas provas dos resultados, as quais devem ser buscadas, se necessário for, em livros de
Linguagens Formais.
1
CAPÍTULO 1. FUNDAMENTOS DE LINGUAGENS FORMAIS
1.1
2
Nı́veis lingüı́sticos
A disciplina da Teoria das Linguagens Formais começou com os trabalhos de Chomsky
(1956 e 1959). Chomsky como lingüista estudava formalismos para descrever linguagens
naturais (Português, Inglês, etc). Naquela época, Chomsky definiu uma hierarquia para
classificação das linguagens. A partir desta classificação iniciou-se a disciplina de Linguagens Formais. Em 1960 a sintaxe do ALGOL foi especificada usando uma BNF que um
formalismo do tipo gramática livre contexto (tipo 2 na classificação de Chomsky).
Na tabela abaixo apresentamos a hierarquia de Chomsky para as linguagens, onde
cada tipo de linguagem define um nı́vel lingüı́stico que está associado a um mecanismo
de reconhecimento:
tipo
tipo
tipo
tipo
tipo
0
1
2
3
nome
nı́vel lingüı́stico
reconhecedor
sem restrições
semântico e pragmático
máquina de Turing
sensı́vel ao contexto
semântico
gramática de atributos
livre de contexto
sintático
autômato de pilha
regular
léxico
autômato
Gramáticas são formalismos usados para especificar linguagens. Linguagens são descritas em diferentes nı́veis, visando facilitar o seu estudo. Na tabela acima mencionamos
quatro nı́veis de especificação de linguagens, que se aplicam tanto a linguagens de computadores como a linguagens naturais. Aqui, estudaremos em detalhes três destes nı́veis:
léxico, sintático e semântico.
• O nı́vel léxico é associado a especificação das palavras de uma linguagem,
também chamadas de tokens. Separar uma frase em palavras e sı́mbolos de pontuação; ou separar uma linha de comandos em tokens ( identificadores, operadores,
delimitadores, etc.) é atividade deste nı́vel.
• O nı́vel sintático é associado a construção de frases e comandos. Verificar a
sintaxe de uma frase (sujeito, verbo e objeto); ou verificar a sintaxe de uma linha
de comandos de uma linguagem de programação é atividade deste nı́vel.
• O nı́vel semântico estuda a semântica, significado ou tradução de uma frase.
Estudar a tradução de uma linha de comandos para uma linguagem de mais baixo
nı́vel; ou a tradução de frases de uma lı́ngua para outra é atividade deste nı́vel.
Um formalismo de um nı́vel superior (mais próximo do tipo 0) tem o poder expressivo
para definir todos os outros nı́veis inferiores. Por exemplo, com um formalismo livre de
contexto podemos definir uma gramática regular, mas o inverso não é verdadeiro. Cada
tipo de linguagem (0, 1, 2 e 3) está associado um mecanismo que é usado no reconhecimento daquele nı́vel da linguagem. Os mecanismos dos nı́veis gramaticais inferiores são
mais eficientes para computação.
Chomsky definiu uma hierarquia para linguagens, com base em regras de produção
que eram usadas para especificar gramáticas de linguagens naturais. Regras de produção
CAPÍTULO 1. FUNDAMENTOS DE LINGUAGENS FORMAIS
3
começaram a ser usadas para descrever a sintaxe de linguagens de programação por J.
Backus e P. Naur, no inı́cio dos anos 60, na especificação da linguagem ALGOL 60. A
notação que foi usada por eles é conhecida como BNF (Bakus-Naur Form).
Linguagens do tipo regular podem ser descritas por BNFs, mas também são descritas por expressões regulares que foram inicialmente introduzidas por Kleene (1959).
Para estas expressões usamos a seguinte notação: P Q denotando P seguido de Q; P |Q
denotando P ou Q; P ∗ denotando zero ou várias ocorrências de P; P + denotando pelo
menos uma ocorrência de P; [] denotando o string vazio. Por exemplo, a(b|c)∗ denota a
linguagem {a, ab, ac, abb, abc, ...}.
Gramáticas sensı́veis ao contexto são descritas por formalismos semânticos associados
aos formalismos sintáticos: cada unidade de especificação de sintaxe é associada a uma
unidade de especificação de semântica. Esta idéia nasceu em 1961, quando Irons construiu
um ”compilador guiado pela sintaxe” para o ALGOL 60. Para Irons um compilador é
especificado com um conjunto de procedimentos cada qual associado a uma unidade
sintática.
Especificações semânticas dentro de um paradigma declarativo, fazem uso do formalismo Gramática de Atributo (GA), que associa equações semânticas às produções gramaticais. Knuth (1968) deu um tratamento formal para GAs, sistematizando os trabalhos
anteriores de especificação de semântica associada a estruturas sintáticas. A linguagem
Prolog executa diretamente GAs, como exemplificaremos neste capı́tulo. Nos próximos
capı́tulos mostraremos também como executar GAs em linguagens imperativas (C++,
Pascal e Java).
Antes de estudarmos a implementação prática de ferramentas para os diferentes nı́veis
lingüı́sticos revisamos conceitos técnicos e resultados das disciplinas de Linguagens Formais e de Compiladores. Os conceitos aqui apresentados são detalhados em em livros
como: [1], [12] e [14].
Exercı́cio 1.1.1 O que é especificado no nı́vel léxico, sintático e semântico de uma linguagem?
Exercı́cio 1.1.2 Quais são os tipos de linguagens formais definidos por Chomsky? Com
que tipo de mecanismo de reconhecimento cada um está associado?
Exercı́cio 1.1.3 Qual é o formalismo gramatical mais adequado para especificar cada
um dos nı́veis de uma linguagem: léxico, sintático e semântico?
Solução: O nı́vel léxico por uma gramática regular; o sintático por uma gramática livre
de contexto e o semântico por uma gramática sensı́vel ao contexto.
Exercı́cio 1.1.4 Em que época e como iniciou a disciplina de Linguagens Formais? (2
linhas)
Exercı́cio 1.1.5 Quando uma linguagem formal foi primeiramente usada na especificação de uma linguagem de programação?
CAPÍTULO 1. FUNDAMENTOS DE LINGUAGENS FORMAIS
1.2
4
Notações gramaticais:BNF (Backus Naur Form)
Abaixo temos uma gramática na notação BNF especificando uma linguagem do tipo
regular para os números binários (1|0)∗ = {0, 1, 00, 10, 11, ...}.
BNF
<numBinario> ::= <digito> <numeroBinario>
<numBinario> ::= λ
<digito> ::= 0 | 1
notação para produções
G --> D G
G --> []
D --> 0 | 1
Regras gramaticais são chamadas de produções – participam na produção de sentenças. Uma produção tem a forma LHS --> RHS, onde lemos (left hand side) e (right
hand side). Por exemplo:
1
2
LHS --> RHS
N --> D G
O lado direito (LHS) define um nome para um não terminal que nomeia o corpo da
regra (RHS). Numa forma básica de abstração, podemos pensar uma produção similar a
um procedimento imperativo onde o LHS é um nome e o lado direito são as chamadas a
subprocedimentos dentro do seu corpo como segue. Esta é a semântica imperativa para
regras de produção.
1
2
3
4
5
procedure <numBinario>;
begin
<digito>
<numBinario>
end;
Para um mesmo nome (LHS) podemos ter várias regras alternativas que podem ser
escritas de duas formas, como segue:
• como em D-->0|1 (que se lê: D gera 0 ou 1) ou
• fazendo uso de duas regras, por exemplo, D-->0, D-->1.
Produções podem ser recursivas, quando no corpo se faz referencia ao nome da produção, por exemplo G-->D G; e, produções podem ser vazias (ter um elemento vazio como
corpo) por exemplo G-->[].
Formalmente, uma gramática é um tupla na forma <G,N,T,P> onde G é sı́mbolo
inicial, N é conjunto dos sı́mbolos não terminais (ou variáveis) N={G,D}. T é o conjunto
dos sı́mbolos terminais T={0,1} e P é o conjunto das regras de produção. O alfabeto da
linguagem compreende os sı́mbolos terminais.
Uma derivação é uma seqüência de sı́mbolos, terminais ou não terminais, gerada a
partir do sı́mbolo inicial, aplicando-se uma mais produções. Por exemplo, na derivação
CAPÍTULO 1. FUNDAMENTOS DE LINGUAGENS FORMAIS
5
G-->DG-->DDG-->DDDG-->1DDG-->10DG-->101G-->101
a regra G --> DG gera a derivação: DG
e, novamente, com mais duas aplicações desta regra gera-se a derivação: DDDG;
a partir de DDDG, com as regras D -->1 e D-->0 chega-se à 101G e, com a regra G-->[],
chega-se à 101.
Uma derivação formada apenas por sı́mbolos terminais é chamada de sentença (101)1 .
Uma derivação formada também com não terminais é chamada de forma sentencial
(10DG). O número de sı́mbolos de uma forma sentencial é chamado de comprimento
(|10DG| = 4).
Uma derivação pode ser representada numa árvore de derivação, ou árvore sintática,
cuja raı́z é o sı́mbolo inicial e cuja fronteira da árvore é a sentença gerada. Cada regra
gramátical usada num passo de derivação é uma relação pai-filho(s) na árvore. Segue
uma árvore de derivação parcial, com a aplicação de três regras, com fronteira 1DG; e,
uma árvore para a sentença 101.
G
/ \
D
G
/
/ \
1
D
G
G
/ \
D
G
/
/ \
1
D
G
/
/ \
0
D
G
/
|
1
[ ]
G-->DG-->DDG-->1DG
G-->DG-->DDG-->DDDG-->1DDG-->10DG-->101G-->101
Numa árvore incompleta (onde ainda existem derivações a serem executadas) a fronteira corresponde a uma forma sentencial. Numa árvore completa a fronteira corresponde a uma sentença. Uma linguagem é o conjunto de todas as sentenças geradas pela
gramática.
O poder expressivo de uma gramática está associado ao poder que um número
finito de regras tem para expressar um número infinito de sentenças. Por exemplo, a
gramática G, fazendo uso de 5 produções, define uma linguagem que compreende todos
os possı́veis valores binários, que é um conjunto infinito.
Exercı́cio 1.2.1 Defina os termos de linguagens formais listados abaixo? (com no máximo
10 palavras, para cada um)
1. BNF
2. produção
1
Uma gramática no nı́vel léxico gera palavras; no nı́vel sintático gera sentenças.
CAPÍTULO 1. FUNDAMENTOS DE LINGUAGENS FORMAIS
6
3. gramática
4. linguagem
5. alfabeto
6. derivação
7. sentença
8. forma sentencial
9. árvore sintática
1.3
Hierarquia de Chomsky
Chomsky classificou as linguagens a partir da complexidade das produções que as
definem. Seja a gramática G=(N,T,P,V) e seja α e β formas sentenciais (strings formados
por não terminais e/ou terminais). Então dizemos que uma linguagem é do:
• tipo 0 (sem restrição): α → β , onde α e β são formas sentencias; β pode ser vazia.
• tipo 1 (sensı́vel ao contexto): α → β, onde em todas as produções o comprimento
de α é menor ou igual ao comprimento de β; salvo para produções vazias;
• tipo 2 (livre de contexto): A → β, onde A é um único não terminal e β é uma
forma sentencial.
• tipo 3 (regular): A → a ou A → aB, onde a é um terminal (podendo ser o vazio);
e, A e B são não terminais.
Vamos examinar alguns exemplos destes tipos de linguagens com suas gramáticas
associadas. O objetivo destes exemplos é desenvolver uma intuição sobre a classificação
de linguagens. Segue uma lista de exemplos:
• regulares: (0|1)∗ ={[],0,1,01,10,...}; a∗ b∗ = {[], a, b, ab, aa, bb, ...};
• livres de contexto: an bn = {ab, aabb, aaabbb, ...};
• sensı́veis ao contexto: an bn cn = {abc, aabbcc, ...}; an bm cn dm = {abcd, abbcdd,
... }; {x x | x ∈ (0|1)∗ };
• sem restrição (ou irrestrita): {hn f n! } onde o comprimento de f representa a computação do fatorial do comprimento de h, {[]f, hf, hhf f, hhhf f f f f f, ..., hn f n! }.
CAPÍTULO 1. FUNDAMENTOS DE LINGUAGENS FORMAIS
1.3.1
7
Sem restrições
Acima apresentamos um exemplo de linguagem irrestrita que tem o poder para calcular o fatorial – linguagens do tipo 0 são associadas a uma Máquina de Turing. No
entanto, na prática, gramáticas do tipo sem restrições, escritas na forma de produções,
são pouco usadas para descrever linguagens de programação pois são difı́ceis de serem
lidas e especificadas.
Segue um exemplo de uma gramática irrestrita, que não possui restrições na escrita
de produções:
1
2
3
4
S --> ab | aASb
A --> bSb | []
AS --> bSb
aASAb --> aa
Esta gramática gera sentenças tais como: {aa, ababbb, ...}. Esta gramática é irrestrita
porque tem uma produção na forma α → β onde o comprimento de α é maior que o de
β ( |aASAb| > |aa| ).
1.3.2
Sensı́vel ao contexto
O nome de gramática sensı́vel ao contexto é motivado pela regra que segue
αAγ → αβγ
onde o não terminal A é substituı́do por β no contexto definido à esquerda por α e à
direita por γ. Por exemplo, abaixo temos uma gramática sensı́vel ao contexto com 13
produções para a linguagem {w w | w ∈ (0|1)∗ } = 0 0, 1 1, 10 10, 01 01, 11 11,
00 00, ...
1
2
3
4
5
6
7
8
S --> ABC
AB --> 0AD
DC --> B0C
EC --> B1C
C --> []
D0 --> 0D,
D1 --> 1D,
0B --> B0,
| 1AD | []
E0 --> 0E
E1 --> 1E
1B --> B1
%1
%2,3,4
%5
%6
%7
%8,9
%10,11
%12,13
Outro exemplo de gramática sensı́vel ao contexto é dado abaixo para a linguagem
a b c = {[], abc, aabbcc, ...}.
n n n
1
2
3
4
S-->abc | []
ab -->aabbC
Cb -->bC
Cc -->cc
CAPÍTULO 1. FUNDAMENTOS DE LINGUAGENS FORMAIS
8
Uma derivação para esta linguagem é dada abaixo:
S-->abc-->aabbCc-->aaabbCbCc-->aaabbCbcc-->aaabbbCcc-->aaabbbccc.
1.3.3
Livre de contexto
A gramática livre de contexto L = an bn é descrita abaixo, com apenas duas produções.
1
2
L --> a L b
L --> [].
Para nós, o estudo destas linguagens tem como objetivo o desenvolvimento de noções
intuitivas para a classificação de linguagens de programação. Por exemplo, as linguagens
{an }, {an bn } e {an bn cn } são respectivamente regular (tipo 3), livre de contexto (tipo 2)
e sensı́vel ao contexto (tipo 1). Já a linguagem {an bn cn dn } continua sendo sensı́vel ao
contexto (tipo 1).
Uma linguagem equivalente a an bm cn dm é a linguagem de parênteses não corretamente aninhados (n [m )n ]m , trocando-se ”abcd” por ”( [ ) ]”. Esta linguagem é sensı́vel
ao contexto. Porém, existem algumas linguagens bem próximas a esta que são do tipo
livre de contexto: (n )n [m ]m e (n [m ]m )n . Isto sugere que somente sentenças formadas por
parênteses corretamente aninhados pertencem a linguagens livres de contexto.
Outra linguagem que não é livre de contexto é wcw onde w ∈ (a|b)∗ , gerando sentenças
como: abbcabb. Entretanto, a linguagem wcwr , onde wr é o reverso de w é uma linguagem
livre do contexto, gerando sentenças tais como: abbcbba.
1.3.4
Regular
A gramática regular a*b* = {[], a, b, ab, aa, bb, ...} é descrita pelas cinco
(ou três) regras dadas abaixo.
1
2
3
R --> A B.
A --> a A | [].
B --> b B | [].
A seguir veremos algumas variações de gramáticas regulares, com base em alguns
resultados teóricos. Uma gramática regular na classificação de Chomsky é definida numa
forma um pouco limitada. Podemos definir outras subclasses de gramáticas regulares:
• linear recursiva à direita (GLD) : A-->wB, A-->w
• linear recursiva à esquerda (GLE) : A-->Bw, A-->w
CAPÍTULO 1. FUNDAMENTOS DE LINGUAGENS FORMAIS
9
Se w tem comprimento menor ou igual a um, então a GLE é também chamada de
Unitária à Esquerda (GLUE) e a GLD é chamada de Unitária à Direita (GLUD). Temos
então cinco subclasses de gramáticas regulares (regular sem restrição na escrita das produções, GLD, GLE, GLUE e GLUD). Como resultado teórico estas diferentes sub-classes
são equivalentes.
Resultado 1.3.1 Duas gramáticas são equivalentes se geram a mesma linguagem.
Resultado 1.3.2 As diferentes classes de gramáticas regulares são equivalentes.
Isto significa que se temos uma gramática regular recursiva à esquerda podemos
escreve-la como recursiva à direita; e, se temos uma linear simples podemos escreve-la
como unitária; e, assim por diante.
A gramática linear à esquerda, que segue, gera a expressão regular (a|b)*(aa|bb)*.
Esta gramática pode ser rescrita como GLUD e GLUE, como solicitado nos exercı́cios
abaixo.
1
2
S --> Aaa | Abb
A --> Aa | Ab | []
Exercı́cio 1.3.1 Rescreva a gramática S acima como uma GLUD.
Exercı́cio 1.3.2 Rescreva a gramática S acima como uma GLUE.
Exercı́cio 1.3.3 Rescreva a gramática R=a*b*, dada acima, como GLUD.
Cabe notar que uma gramática de uma classe pertence também as suas classes superiores na hierarquia de Chomsky: regular ⊆ livre de contexto ⊆ sensı́vel ao contexto ⊆
irrestrita. Porém, sempre devemos rescrever uma gramática buscando classifica-la com a
menor categoria possı́vel, pois, as ferramentas de processamento das categorias inferiores
são mais eficientes e simples de serem implementadas.
Exercı́cio 1.3.4 Quando duas gramáticas são equivalentes?
Exercı́cio 1.3.5 Caracterize os tipos de linguagens 0, 1, 2, 3. Como eles se diferenciam?
Dê dois exemplos de linguagens para cada tipo, na notação de conjuntos de sentenças.
Exercı́cio 1.3.6 Porque uma gramática tipo 1 é chamada sensı́vel ao contexto? (2 linhas)
CAPÍTULO 1. FUNDAMENTOS DE LINGUAGENS FORMAIS
1.4
10
Mais sobre classificação de linguagens
Os tipos de gramática 1, 2 e 3 são amplamente estudados na disciplina de Linguagens
Formais, dentro dos cursos de Ciências da Computação, onde estuda-se os formalismos
gramaticais (para descrever e estudar linguagens) e suas máquinas associadas (para implementar ferramentas).
Além da hierarquia de Chomsky, apresentamos abaixo uma classificação de gramáticas
associadas aos três nı́veis de especificação de linguagens. Esta classificação simplifica a
hierarquia de Chomsky reunindo numa mesma classe os tipos de linguagens 0 e 1. Estes
dois tipos de linguagens podem ser especificados por uma gramática de atributos (GA)
ou uma DCG, como será ilustrado a seguir.
Semântico - Gramáticas de Atributos - GA (tipo 0)
Definite Clause Grammar - DCG (tipo 0)
Gramáticas sensı́veis ao contexto (tipo 1)
Sintático - Gramáticas livres de contexto (tipo 2)
Léxico - Gramáticas regulares (tipo 3)
Estas três categorias estão associadas a diferentes tipos de processamento necessário
para manipular linguagens naturais ou de programação: léxico, sintático e semântico.
• No processamento léxico são identificados os tokens básicos de uma linguagem, por
exemplos, os identificadores, os números, os delimitadores, etc. São produzidas fitas
de palavras para o processamento sintático.
• No processamento sintático são verificados os erros de sintaxe, por exemplo, indicando a falta de um parêntese. A análise sintática permite construir árvores sintáticas
(abstratas) como representações intermediárias para o processamento semântico.
• No processamento semântico são identificados erros tais como: variável não declarada. O processamento semântico refere-se principalmente a uma tradução das
construções da linguagem para alguma forma executável, tipicamente uma linguagem de mais baixo nı́vel.
Segue abaixo a tentativa de especificação da linguagem an bm cn dm , sensı́vel ao contexto,
com produções do tipo livre de contexto (apenas um não terminal no lado esquerdo das
produções).
1
2
3
4
S --> A(1) B(1) C(1) D(1).
S --> A(1) B(2) C(1) D(2).
....
A(1) --> a. A(2) --> aa. ...
CAPÍTULO 1. FUNDAMENTOS DE LINGUAGENS FORMAIS
5
6
B(1) --> b.
...
B(2) --> bb.
11
...
Como vemos, precisamos de um número infinito de regras de produção. A regra
S aponta para uma solução, uma produção parametrizada, tal como S --> A(n) B(m)
C(n) D(m). Este problema também acontece com a gramática abaixo que tenta descrever
a linguagem do tipo 0 para a computação do fatorial. O cálculo do fatorial, {[]f, hf, hhf f,
hhhf f f f f f, ..., hn f n! }, além de parâmetros, precisa também de uma ”anotação” tipo
uma restrição de igualdade: M é igual ao fatorial de N.
1
2
3
F-->H(N) F(M) {onde M=N!}.
H(1)--> h
H(2)--> hh
4
5
6
7
8
...
F(1)-->f
F(2)-->ff
...
Estes problemas, especificados com infinitas produções, contradizem o princı́pio do
uso de gramáticas que é de definir infinitas sentenças a partir de um número finito de
regras de produção. Isso já era esperado: uma gramática sensı́vel ao contexto não pode
ser descrita apenas com regras livres de contexto.
O formalismo de gramática de atributos (GA) estende uma gramática livre de contexto com mecanismos para descrever semântica. A idéia é manter a simplicidade das
gramáticas livres de contexto adicionando parâmetros e equações para aumentar o poder
computacional da notação livre de contexto até alcançar o poder de uma máquina de
Turing. Por exemplo, a linguagem an bn cn , é facilmente descrita em GA, a partir de uma
versão livre do contexto para a linguagem regular a∗ b∗ c∗ , como segue.
1
2
3
4
5
6
7
G
A
A
B
B
C
C
-->A B C
-->a A
-->[]
-->b B
-->[]
-->c C
-->[]
Nesta versão sintática da gramática não temos a restrição que devemos ter o mesmo
números de as, bs e cs. Abaixo temos uma solução como GA. Inicialmente as, bs e cs
são contados nas variáveis x, y e z. Na contagem lemos assim, A(0)-->[] – o números
de as numa produção vazia é zero; A(x+1)-->a A(x) – o número de as numa produção
recursiva é um (do terminal) mais o números de as do não terminal. No final da produção
CAPÍTULO 1. FUNDAMENTOS DE LINGUAGENS FORMAIS
12
principal temos uma equação que diz que eles devem ter um mesmo valor {x=y=z}2 . A
produção G é válida numa derivação somente se for válida a equação {x=y=z}.
1
2
3
4
5
6
7
G
-->
A(x+1)-->
A( 0 )-->
B(y+1)-->
B( 0 )-->
C(z+1)-->
C( 0 )-->
A(x)B(y)C(z) {x=y=z}
a A(x)
[]
b B(y)
[]
c C(z)
[]
Podemos comparar esta versão em GA com outra versão equivalente representada por
produções do tipo sensı́vel ao contexto, da seção 1.3.2. As especificações em GA são mais
fáceis de serem lidas e entendidas.
Com GA, podemos especificar qualquer tipo de linguagem, por exemplo, a linguagem
da computação do fatorial, {[]f, hf, hhf f, hhhf f f f f f, ..., hn f n! }, pode ser descrita de
forma similar contando-se hs e fs e, fazendo-se uma restrição na produção principal y=x!,
como segue.
1
2
3
4
5
G
-->H(x) F(y){x!=y}
H(x+1)-->h H(x).
H( 0 )-->[]
F(y+1)-->f F(y).
F( 0 )-->[]
Nesta solução a computação do cálculo do fatorial não é executada pelo mecanismo
gramatical, que é livre do contexto, mas sim por um mecanismo extra-gramatical que
executa as equações da GA.
A seguir estudaremos como programar os três principais tipos de gramáticas: regulares, livres de contexto e gramáticas de atributos.
Exercı́cio 1.4.1 O que é uma gramática de atributos? Corresponde a que tipo de gramática
na classificação de Chomsky?
Exercı́cio 1.4.2 Qual o formalismo gramatical mais adequado para especificar cada um
dos nı́veis de uma linguagem: léxico, sintático e semântico?
1.5
Gramáticas em Prolog: DCG
Prolog possui um mecanismo embutido na sua notação de cláusulas definidas, para
processar gramáticas. Este mecanismo, conhecido como DCG: Definite clause grammar,
permite a codificação direta de GAs, como regras executáveis em Prolog.
2
Note que estamos usando as chaves {} com dois significados, um para denotar linguagens como conjuntos de sentenças e outro para denotar restrições semanticas associadas às produções de uma gramática.
CAPÍTULO 1. FUNDAMENTOS DE LINGUAGENS FORMAIS
13
Resultado 1.5.1 As DCGs (Definite Clause Grammars) tem o mesmo poder computacional que as GAs (gramáticas de atributos).
As DCGs (como GAs) são baseadas em gramáticas livres de contexto. Uma DCG, no
Prolog padrão, processa gramáticas livres de contexto sem recursividade à esquerda. Por exemplo, a regra R-->Ra que é recursiva à esquerda deve ser rescrita como
R-->aR antes de ser codificada como regra DCG. De modo similar a regra R-->[]|aR,
com uma alternativa vazia, deve ser rescrita como R-->aR|[] onde a alternativa vazia é
a derradeira.
1.5.1
Gramática regular
A gramática R = a∗ b∗ apresentada anteriormente é traduzida para as regras DCG,
que seguem:
1
2
3
4
5
r
a
a
b
b
-->
-->
-->
-->
-->
a, b.
[a],a.
[].
[b],b.
[].
Os sı́mbolos terminais são representados entre colchetes. Os não terminais são representados por letras minúsculas (pois em Prolog maiúsculas são variáveis).
Dada uma DCG podemos perguntar sobre as sentenças da linguagem gerada pela
gramática. Sabemos que a gramática R gera as sentenças {a,b,aa,bb,ab,aab,abb,...}
e que não são válidas as sentenças {ba,aba,...}. Portando, podemos perguntar:
?- r([a,b,b],X).
X=[], Yes
?- r([a,b,a],[]).
NO
?- r([a,b,a],X).
X=[a]
Yes
As sentenças são representadas em listas de sı́mbolos terminais. Numa pergunta são
passados dois argumentos: uma cadeia de entrada e uma cadeia de saı́da, respectivamente.
Em ?-r([a,b,b],X). X=[], Yes a saı́da é vazia (X=[]), significando que toda a entrada
foi reconhecida (ou consumida). Caso contrário, o conteúdo do argumento de saı́da é o
que sobrou (deixou de ser reconhecido); em ?-r([a,b,a],X). X=[a]; o "a" deixou de
ser reconhecido.
Podemos testar as produções de maneira isolada, por exemplo, a pergunta ?- b([b,b,
b,a,a],X). X=[a,a], Yes é feita para a produção b, que reconhece uma seqüência de
b(s), e sobraram dois a(s).
CAPÍTULO 1. FUNDAMENTOS DE LINGUAGENS FORMAIS
14
Exercı́cio 1.5.1 O que é uma DCG? Corresponde a que tipo de gramática na classificação de Chomsky?
Solução: Uma DCG (Definite Clause Grammars) é um formalismo gramatical que é
embutido na linguagem Prolog. Corresponde a uma Gramática de Atributos que tem o
poder computacional de uma linguagem tipo sensı́vel ao contexto.
Exercı́cio 1.5.2 Uma gramática é definida por uma tupla (G,N,T,P). Num código DCG
como identificamos G,N,T e P?
Solução: G é o sı́mbolo inicial, normalmente corresponde a primeira produção. P é o
conjunto das produções, normalmente codifica-se cada produção em uma linha. No lado
direito da produção os elementos são separados por uma conjunção lógica (,) vı́rgula.
Cada terminal (T) é codificados entre colchetes, no lado direito de uma regra. Todos os
não terminais (N) são codificados como cabeças de regras, com pelo menos uma regra,
podendo ter várias regras alternativas (normalmente, uma em cada linha terminada por
ponto). Duas regras podem ser codificadas com uma mesma cabeça, neste caso os dois
corpos da regra são ligados por uma disjunção lógica (;).
1.5.2
Gramática livre de contexto
Podemos codificar a gramática L = an bn , como segue.
1
2
l --> [a],l,[b].
l --> [].
Seguem alguns testes para esta gramática L.
?- l([a,b],X). X=[], Yes
?- l([a,a,a,b,b,b],[]). Yes
?- r([a,b,a],[]). NO
Podemos promover uma gramática regular (tipo 3) para livre de contexto (tipo 2)
usando equações e atributos de GAs. A gramática regular R = a∗ b∗ , apresentada anteriormente, é codificada em GA, na notação DCG como:
1
2
3
4
5
r
a
a
b
b
-->
-->
-->
-->
-->
a, b.
[a],a.
[].
[b],b.
[].
Se adicionarmos alguns parâmetros nas produções da gramática regular R podemos ter
uma nova gramática do tipo livre de contexto, equivalente a gramática l-->[a],l,[b]|[].
CAPÍTULO 1. FUNDAMENTOS DE LINGUAGENS FORMAIS
1
2
3
4
5
15
r --> a(X), b(Y),{X=Y}.
a(N+1) --> [a],a(N).
a( 0 ) --> [].
b(N+1) --> [b],b(N).
b( 0 ) --> [].
1.5.3
Gramática sensı́vel ao contexto
A gramática sensı́vel ao contexto, S = an bm cn dm , não pode ser programada por
produções do tipo livre do contexto. No entanto, em regras DCG é fácil descrever esta
linguagem S como segue.
1
2
3
4
5
6
7
8
9
s --> a(X),b(Y),c(Z),d(W), {X=Z, Y=W}.
a(N+1)-->[a],a(N).
a( 1)-->[a].
b(N+1)-->[b],b(N).
b( 1)-->[b].
c(N+1)-->[c],c(N).
c( 1)-->[c].
d(N+1)-->[d],d(N).
d( 1)-->[d].
Nesta codificação cada uma das regras a,b,c,d conta o número de ocorrências dos
caracteres (tokens) e a equação semântica em s, {X=Z, Y=W}, força a ocorrência do mesmo
número de acs e de bds. Seguem alguns testes para a gramática S:
?- s([a,b,c,d],X). X=[], Yes
?- s([a,b,b,c,d,d],[]). Yes
?- s([a,b,a],[]). NO
Exercı́cio 1.5.3 Escreva uma GA pra a linguagem {w w | w ∈ (0|1)∗ }.
Solução:
1
2
3
4
5
GLC:
S -->
X -->
X -->
X -->
X X.
0 X.
1 X.
[].
GA na notaç~
ao DCG:
s
--> x(A), x(B),{A=B}.
x([0|N]) --> [0], x(N).
x([1|N]) --> [1], x(N).
x( 0 ) --> [].
Compare esta solução em GA com a versão sensı́vel ao contexto apresentada abaixo
(é a mesma da seção 1.3.2). Lendo as produções abaixo é difı́cil de se entender que
representa a linguagem {w w | w ∈ (0|1)∗ }. Além disso, são usadas 13 produções contra
4 da solução GA.
CAPÍTULO 1. FUNDAMENTOS DE LINGUAGENS FORMAIS
1
2
3
4
5
6
7
8
S --> ABC
AB --> 0AD
DC --> B0C
EC --> B1C
C --> []
D0 --> 0D,
D1 --> 1D,
0B --> B0,
| 1AD | []
E0 --> 0E
E1 --> 1E
1B --> B1
16
%1
%2,3,4
%5
%6
%7
%8,9
%10,11
%12,13
A notação para GA é vista como um formalismo para especificar semântica. Falamos
especificar, pois uma gramática define o que e não o como. Na implementação de uma
GA devemos escolher uma linguagem de programação ou uma ferramenta especializada.
A linguagem mais próxima da notação GA é o formalismo DCG do Prolog, porém nos
próximos capı́tulos veremos como programar GAs também em linguagens imperativas
como C, C++, Pascal e Java.
1.6
Exercı́cios avançados
Exercı́cio 1.6.1 De exemplos em DCG, de codificação e de uso (como testar), para cada
um dos tipos de gramáticas: 0, 1, 2 e 3.
• regular (tipo 3)
• livre de contexto (tipo 2)
• sensı́vel ao contexto (tipo 1)
• irrestrita (tipo 0)
Capı́tulo 2
Fundamentos para GRs, GLCs e
GAs
Neste capı́tulo enunciamos os principais resultados de Linguagens Formais relacionados a gramáticas regulares (GRs), livres de contexto (GLCs) e gramáticas de atributos
(GAs).
Inicialmente revisamos as noções e fundamentos sobre as linguagens regulares (GRs e
expressões regulares) e o formalismo computacional associado (autômatos). O principal
tópico da primeira seção é a transformação de uma gramática regular para um autômato
e vice-versa.
GLCs são processadas por autômatos de pilha. Quando usamos um autômato de pilha
para processar uma GLC, estamos implementando um método de análise sintática que
constrói uma árvore sintática para uma string de entrada.
Existem duas principais subclasses de gramáticas livres de contexto (GLCs), uma é
chamada LL(Left to right, Left most derivation) e a outra é chamada LR(Left to right,
Right most derivation). A grande diferença entre elas é que a classe LL é adequada para
a análise descendente (top-down) e a classe LR é adequada para a análise ascendente
(bottom up).
Na segunda seção, enquanto apresentamos os fundamentos da análise sintática, falamos de conceitos e técnicas usados na preparação de uma gramática para programação:
ambigüidade, remover recursividade à esquerda, precedência e associatividade de operadores.
Na terceira seção apresentamos as GAs. Sobre GAs falamos sobre classificação dos
atributos como herdados e sintetizados. Introduzimos duas subclasses de GAs: S-GA
e L-GA. A primeira só com atributos sintetizados e a segunda também com atributos
herdados, porém com a restrição que as equações possam ser calculadas junto com a
análise sintática de uma gramática do tipo LL.
Por fim, numa seção especial, e opcional para uma primeira leitura, falamos de
gramáticas reversı́veis, que podem ser usadas para reconhecimento e/ou geração de sentenças.
17
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
2.1
18
Gramáticas Regulares e Autômatos
Existem diferentes métodos sistemáticos para se programar as gramáticas regulares e
os seus formalismos equivalentes, as expressões regulares e os autômatos finitos. Aqui,
nós veremos três principais métodos para programação de gramáticas regulares, cada um
mais adequado a um tipo de formalismo:
• com goto, mais adequado à codificação de autômatos;
• iterativo, mais adequado à codificação de expressões regulares;
• recursivo, mais adequado à codificação de regras de produção (gramáticas regulares).
Para a escolha do método, num projeto de programação de uma gramática, devemos
considerar os resultados da disciplina de Linguagens Formais, bem como a dificuldade
técnica de se passar de um formalismo para outro. Estes temas são apresentados na
seqüência.
2.1.1
Transformação de expressão regular para autômato
Temos um resultado que diz que uma expressão regular pode ser reconhecida por
um autômato. No entanto, obter um autômato resultante bom (com poucos estados e
determinı́stico) pode ser um tanto trabalhoso.
Resultado 2.1.1 Toda linguagem gerada por uma expressão regular é também gerada
por uma gramática regular (e vice versa).
Resultado 2.1.2 Toda linguagem regular é reconhecida por um autômato finito determinı́stico (e vice versa).
Uma expressão regular é definida por quatro construtores básicos como segue (às
vezes são utilizados outros construtores, como por exemplo, os colchetes para denotar
uma construção opcional):
• seqüência: ab denota a seguido de b;
• repetição: a* denota {a, aa, aaa, ...};
• alternativa: a|b denota um a ou um b;
• agrupamento (abstração): x(a|b)*y a expressão entre parênteses denota o agrupamento, a repetição é aplicada ao agrupamento.
Inicialmente ilustramos o uso do algoritmo de Thompson [1] para obter um autômato
com transições vazias a partir de uma expressão regular, com as regras abaixo:
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
19
• uma palavra a : cria-se um subautômato com dois estados e a na transição entre
eles;
• uma seqüência ab : cria-se uma transição vazia entre os dois subautômatos que
implementam a e b;
• uma repetição a* : cria-se um arco, de transição vazia, do para o inı́cio do subautômato a;
• uma alternativa a|b : cria-se dois arcos alternativos, unidos no inı́cio e no fim, um
para o subautômato a e outro para o subautômato b; equivale a ligar os dois inı́cios
e os dois fins;
• um agrupamento (abstração) x(a|b)*y: cria-se um novo subautômato, com a entrada de x que precede e com a saı́da em y que sucede;
• por fim: remove-se as transições vazias sem utilidade, que não afetam o comportamento do autômato gerado (passo opcional – só para otimização).
A Figura 2.1 apresenta uma versão para a expressão regulara*b*, onde o autômato
já está simplificado, removemos algumas transições vazias. O estado sa repete o a e o
estado sb repete o b. Os dois estados são ligados por uma transição vazia.
Outro exemplo é mostrado abaixo, Figura 2.2, para a expressão regular (a|b)*abb.
Este exemplo mostra uma construção alternativa dentro de uma repetição e também
seqüências não opcionais abb. A Figura 2.3 mostra uma versão do autômato simplificado,
sem as transições vazias.
Figura 2.1: Autômato finito correspondente a a expressão regular a*b*; obtido pelo algoritmo
de Thompson.
Abaixo apresentaremos outras versões destes autômatos, gerados de outras formas.
2.1.2
Transformação de gramática regular para autômato
Resultado 2.1.3 Toda gramática regular na forma GLUD é equivalente a um autômato,
onde cada produção equivale a uma transição; e cada não terminal a um estado.
Se o léxico é especificado como uma gramática regular, e queremos programa-lo como
um autômato, devemos fazer a conversão entre as produções da gramática regular e o
autômato. O resultado acima diz que a conversão é direta se a gramática esta na forma
GLUD.
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
20
Figura 2.2: Autômato finito não determinı́stico, obtido pelo método de Thompson, a partir da
expressão regular (a|b)*abb.
Figura 2.3: Autômato finito não determinı́stico para a expressão regular (a|b)*abb, sem as
transições vazias
Abaixo exemplificamos o processo de conversão, para uma gramática regular que
define a linguagem gerada pela expressão regular a*b*. Usamos a notação DCG para
escrever diferentes versões da gramática regular para efeito de estudo. Segue uma versão
inicial onde renomeamos os sı́mbolos não terminais com o prefixo (s) para representar
estados (sr, sa, sb).
1
2
3
4
5
6
%% vers~
ao inicial
R --> A B
A --> a A
A --> []
B --> b B
B --> []
%%
sr
sa
sa
sb
sb
vers~
ao DCG
--> sa,sb.
--> [a],sa.
--> [].
--> [b],sb.
--> [].
Um método para codificar uma gramática regular num autômato é transforma-la
numa gramática equivalente na forma GLUD (linear unitária recursiva à direita); onde
as produções tem a forma A-->wB ou A-->w (na notação DCG, a-->[w],b ou a-->[w]).
Portanto, iniciamos removendo a produção sr --> sa, sb.
1
2
3
sr --> sa.
sa --> [a],sa.
sa --> sb.
%% 1ro passo
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
4
5
21
sb --> [b],sb.
sb --> [].
Para se chegar a GLUD, removemos também as duas produções do tipo A-->B. Segue
a versão final, onde o sı́mbolo inicial é o sa.
1
2
3
4
5
sa
sa
sa
sb
sb
-->
-->
-->
-->
-->
[a],sa.
[b],sb.
[].
[b],sb.
[].
%% 2do passo
%% vers~
ao GLUD
Uma gramática regular na forma GLUD é facilmente transformada num autômato,
pelas regras que seguem:
• Cada não terminal é mapeado num estado do autômato;
• Cada produção do tipo, X --> [t] Y, é mapeada numa transição do estado X para
o estado Y; onde a transição é marcada com [t];
• Cada produção do tipo X --> [t] é mapeada numa transição do estado X para um
estado final; onde a transição é marcada com [t];
• Cada produção do tipo X --> [] é mapeada numa transição para um estado final;
pode-se simplesmente marcar o estado como final.
Usando estas regras a Figura 2.4 apresentam o autômato gerado para a versão GLUD
da gramática. Definimos um único estado final do autômato, combinando os dois estados
finais. Compare-a com a versão gerada a partir da expressão regular a*b* dada na
Figura 2.1. Ambas as versões são determinı́sticas.
Temos, também, um resultado que garante a existência de uma gramática regular
para cada autômato, portanto, podemos usar estas regras numa forma invertida para
obter uma versão GLUD da gramática.
2.1.3
Transformado uma expressão regular numa gramática regular
Vimos como transformar uma expressão regular num autômato e, também, como
transformar uma gramática regular GLUD num autômato. Veremos ainda como transformar uma expressão regular numa gramática regular.
Uma expressão regular é definida por quatro construtores básicos, seqüência, repetição, alternativa e agrupamento (abstração). Para cada uma destas construções existe
uma notação gramatical equivalente definida em termos das regras:
• seqüência: ab regra A --> ab.
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
22
Figura 2.4: Autômato finito determinı́stico associado a versão GLUD da gramática sr.
• repetição: a* regras A --> aA | [].
• alternativa: a|b regras A --> a|b.
• agrupamento(abstração): x(a|b)y regras G --> xAy; A-->a|b.
Usando estas regras, é relativamente fácil traduzir uma expressão regular num conjunto de produções. Vamos exemplificar o processo para a expressão regular (a|b)*abb.
Inicialmente é necessário desmembrar a expressão regular em termos destas primitivas,
como segue:
1
2
3
4
5
R1
R2
R3
R4
R
=
=
=
=
=
a | b
(R1)*
a R4
b b
R2 R3
Nesta representação cada R, R1, R2, R3 e R4 corresponde a um construtor elementar
de seqüência, repetição ou alternativa. O R compreende a expressão toda. Para obter a
gramática basta aplicar as regras enunciadas acima; segue o resultado.
R1
R2
R3
R4
R
-->
-->
-->
-->
-->
a | b
R1 R2 | []
a R4
b b
R2 R4
Esta gramática é regular mas não esta numa forma GLUD. Para obter a forma GLUD,
inicialmente, juntamos as produções R1 e R2, como segue.
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
23
R2 --> a R2
R2 --> b R2
R2 --> []
Depois reescrevemos a R3 e R4:
R3 --> a R4
R4 --> b R5
R5 --> b
Por fim ligamos as produções R2 e R3, obtendo a forma GLUD. Ainda temos um
problema pois ela é não determinı́stica.
R2
R2
R2
R4
R5
-->
-->
-->
-->
-->
b
a
a
b
b
R2
R2
R4
R5
Podemos facilmente transformar esta gramática regular num autômato não determinı́stico, com 4 estados: s0, s1, s2 e s3(fim).
S0
S0
S0
s1
s2
-->
-->
-->
-->
-->
b
a
a
b
b
s0
s0
s1
s2
(s3=fim)
A Figura 2.3 apresenta o autômato para esta versão não determinı́stica da gramática
GLUD. Abaixo vamos mostrar como obter uma versão determinı́stica para esta mesma
linguagem.
Resultado 2.1.4 Todo autômato finito não determinı́stico pode ser rescrito como um
autômato finito determinı́stico.
Existe um método de conversão de um autômato não determinı́stico para determinı́stico. Ele cria subconjuntos de estados alcançáveis a partir do estado inicial [1].
Vamos ilustrar este processo. Começa-se com o estado inicial A={s0}, que é a semente
para gerar os próximos estados – s0 mais todos os estados alcançáveis com uma transição
vazia (que neste caso não temos); depois, cria-se o conjunto transiç~
oes(A,a)={s0,s1}=B
e o conjunto transiç~
oes(A,b)={s0}=A. Seguindo este processo criamos os conjuntos de
A até D, como segue.
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
inicial=
transiç~
oes(A,a)=
transiç~
oes(A,b)=
transiç~
oes(B,a}=
transiç~
oes(B,b)=
transiç~
oes(C,a)=
transiç~
oes(C,b)=
transiç~
oes(D,a)=
transiç~
oes(D,b)=
{s0
}
{s0,s1}
A
A
{s0,s2}
A
{s0,s3}
A
A
24
= A
= B
= C
= D
Com estas transições e os estados A, B, C e D criamos uma versão determinı́stica do
autômato. O estado final é o D. Esta versão equivale a uma gramática GLUD, determinı́stica, como vemos na Figura 2.5.
A-->a
A-->b
B-->a
B-->b
C-->a
C-->b
D-->a
D-->b
B
A
A
D
A
D
A
A
Exercı́cio 2.1.1 Compare os autômatos, da Figura 2.5, versão determinı́stica, com a
versão não determinı́stica, da Figura 2.3. Compare quanto ao número de estados e
número de transições?
Mostramos como remover o não determinismo de um autômato, no entanto este
método é difı́cil de ser manualmente aplicado para autômatos grandes. Neste caso devemos dispor de uma ferramenta automática que faça este processo.
Figura 2.5: Autômato finito determinı́stico, para a expressão regular (a|b)*abb, obtido pelo método de subconjuntos de estados alcançáveis, a partir da versão não determinı́stica do
autômato, da Figura 2.3.
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
2.1.4
25
Removendo não determinismo, com fatoração
Outra forma de eliminar o não determinismo numa gramática, que é aplicável em
certos casos, é a fatoração das produções. Para exemplificarmos este processo definimos
duas produções não determinı́sticas que geram os inteiros: um inteiro é uma seqüência
de dı́gitos, não vazia.
1
2
int --> dig int
int --> dig
Figura 2.6: Autômato finito para valores inteiros; versões: não determinı́stica e determinı́stica.
Estas duas produções int equivalem a um autômato finito não determinı́stico. Elas
podem ser facilmente reescritas para uma versão determinı́stica, fatorando-se o termo
comum, dig, como segue:
1
2
3
int --> dig rint
rint --> dig rint
rint --> []
Estas duas versões da mesma gramática representam os dois autômatos da Figura 2.6,
sendo que o segundo é determinı́stico. Na primeira versão temos duas transições com dig
que partem do mesmo estado int com o valor dig. Na versão fatorada temos dois estados:
int, rint. A transição entre os dois garante a presença de pelo menos um dı́gito. O
estado rint(resto int) opcionalmente lê uma seqüência de dı́gitos.
A vantagem do autômato determinı́stico é a sua eficiência e maior simplicidade de
implementação. Além disso, ás vezes o mecanismo usado na implementação não permite
a codificação de regras não determinı́sticas.
Existe um método para obter a versão mı́nima de um autômato (ver [1]), neste texto
ele não é apresentado, pois, manualmente, ele só se aplica a pequenos autômatos. Quando
necessário devemos utilizar uma ferramenta automatizada para obter versões minimizadas
de autômatos.
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
26
Resultado 2.1.5 Para um autômato finito determinı́stico existe um outro equivalente mı́nimo, com um número mı́nimo de estados.
Existe um compromisso entre eficiência e tamanho do autômato [1]. Um autômato
finito determinı́stico, associado a uma expressão regular r, ocupa um espaço na ordem de
2|r| , onde |r| é o comprimento da expressão r; este autômato permite o reconhecimento de
uma palavra x numa ordem linear em função do comprimento de x, |x|. Por outro lado,
um autômato não determinı́stico para a mesma linguagem ocupa um espaço na ordem
linear ao comprimento de r e precisa de um tempo, no pior caso, na ordem de |r| × |x|.
Obter um autômato determinı́stico é importante para se conseguir a máxima eficiência.
E, obter uma versão com o número mı́nimo de estados é importante para reduzir o
espaço de memória ocupado pelo autômato. Como já comentamos estas otimizações para
autômatos grandes devem ser executadas com auxilio de ferramentas especializadas.
Exercı́cio 2.1.2 O que é uma expressão regular?
Exercı́cio 2.1.3 Qual é a relação entre uma expressão regular e uma gramática regular?
Exercı́cio 2.1.4 O que é um autômato finito?
Exercı́cio 2.1.5 Qual é a relação entre um autômato finito e uma gramática regular?
Exercı́cio 2.1.6 Qual é a utilidade de se transformar uma gramática regular para a
forma GLUD?
Exercı́cio 2.1.7 Qual a diferença entre um autômato finito determinı́stico e não determinı́stico?
2.2
Gramáticas Livres de Contexto (GLC)
Existem duas principais subclasses de gramáticas livres de contexto (GLC), uma é
chamada LL(Left to right, Left most derivation) e a outra é chamada LR(Left to right,
Right most derivation). Estas duas classes são implementáveis por autômatos de pilha. A
grande diferença delas é que a classe LL é adequada para a análise descendente (top-down)
e a classe LR é adequada para a análise ascendente (bottom up).
Dentro de cada uma destas subclasses podemos ter variações quanto ao número de
tokens de ”lookahead” (que temos que olhar na fita de entrada para tomarmos uma
decisão sobre qual alternativa de uma produção deve ser utilizada). As mais usadas são
a LL(1) e a LR(1), onde é lido apenas um token de lookahead.
Resultado 2.2.1 Para cada gramática livre de contexto existe um autômato de pilha
que a reconhece.
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
2.2.1
27
Análise ascendente LR(k) e descendente LL(k)
Inicialmente enunciamos alguns resultados sobre as duas principais subclasses de
GLCs.
Resultado 2.2.2 Para toda gramática LR(k) existe uma gramática equivalente LR(1).
Resultado 2.2.3 Toda gramática LL(k) é também LR(k).
Resultado 2.2.4 Existem gramáticas LR(k) para as quais não existem gramáticas LL(k’)
equivalentes, para qualquer k’ inteiro finito.
Resultado 2.2.5 Dada uma gramática LR(k), existe um algoritmo que num número
finito de passos diz se existe ou não uma gramática LL(k’) equivalente.
O primeiro resultado 2.2.2 diz que uma gramática LR com (k) sı́mbolos de lookahead
pode ser rescrita como uma gramática de um sı́mbolo de lookahead - equivale ao resultado sobre fatoração de regras. Os resultados 2.2.3, 2.2.4 e 2.2.5 dizem que a classe de
gramáticas LL(k) é um subconjunto da classe LR(k). O resultado 2.2.5 diz que dada uma
gramática LR(k) podemos testar se existe uma gramática equivalente do tipo LL(k’) ou
não.
Para efeito de especificação de linguagens de programação as duas famı́lias são bem
próximas. É muito difı́cil encontramos uma construção sintática de uma linguagem de
programação que possa ser especificada numa gramática LR(k) e não possa ser especificada numa gramática LL(k’).
Neste texto para efeito de programação de GLCs apresentaremos apenas métodos de
programação para a classe LL(k). O problema com a programação de gramáticas LR(k)
é que elas dependem de tabelas que são difı́ceis de serem obtidas manualmente [1]. Em
contraste produções de gramáticas LL(k) são diretamente traduzidas para procedimentos
imperativos.
Antes de implementarmos uma ferramenta com base numa GLC definida por uma
gramática LL(k), devemos examinar as produções da gramática a fim de fazer alguns
ajustes necessários, entre eles:
• remoção de ambigüidade;
• fatoração;
• remoção de recursividade à esquerda (ou à direita);
• tratamento para precedência e associatividade de operadores.
Estes temas são abordados a seguir.
Exercı́cio 2.2.1 Quais são as duas principais classes de GLC?
Exercı́cio 2.2.2 Qual das duas classes é mais geral?
Exercı́cio 2.2.3 Quais os passos (de ajustes) que devemos seguir para programar uma
gramática do tipo LL(k)?
Exercı́cio 2.2.4 Numa gramática LL(k) o que significa o k?
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
2.2.2
28
Recursividade à esquerda ou à direita
Uma gramática para expressões do tipo inteiro, por exemplo, 1+2*3 que é 7; (20+4)*4/8
que é 12, pode ser definida como segue.
1
2
3
E --> E+E | E-E
E --> E*E | E/E
%% ambı́gua
E --> (E) | 1|2|3...
Para se construir analisadores sintáticos esta gramática apresenta alguns problemas:
• é ambı́gua;
• possui recursividade à esquerda (problema para LL(k));
• não expressa o conhecimento relacionado com a precedência dos operadores (*/)
sobre os operadores (+-).
Uma gramática é ambı́gua se podemos construir duas derivações para uma mesma
sentença. Que é o mesmo que construir duas árvores sintáticas para uma mesma sentença,
como segue.
1
2
3
4
5
6
7
8
E
/|\
E + E
/
/|\
1
E * E
/
\
2
3
(a)
E
/|\
E * E
/|\
\
E + E
3
/
\
1
2
(b)
O conhecimento da precedência dos operadores possibilita a construção de uma
árvore abstrata para se avaliar uma expressão: com as operações de menor precedência
mais próximas do topo.
+
/ \
1
*
/ \
2
3
%% 1+2*3 = 1+(2*3)
Esta árvore abstrata corresponde a uma estrutura similar ao exemplo (a) das árvores
sintáticas descritas acima. No exemplo (a) o operador de soma está no topo. Na transformação de uma árvore sintática para uma árvore abstrata, removemos os não terminais
criando uma estrutura mais abstrata, porém similar à árvore sintática. Representamos
somente os operadores e os valores. Até mesmo os parênteses são removidos (estão implı́citos na estrutura da árvore).
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
29
Resultado 2.2.6 (RESTRIÇÃO) Uma gramática livre do contexto LL(k) não pode
ter produções recursivas à esquerda.
Resultado 2.2.7 Para cada gramática livre do contexto com produções recursivas à
esquerda existe outra gramática livre do contexto equivalente sem recursividade à esquerda.
Com base no resultado 2.2.7 pode-se reescrever a gramática apresentada numa versão
sem recursividade à esquerda. Por outro lado, não existe um resultado sobre remoção de
ambigüidade. Algumas GLCs são inerentemente ambı́guas - i.e., não é possı́vel remover
a ambigüidade delas. Ainda assim, é difı́cil encontramos uma gramática inerentemente
ambı́gua para problemas práticos em linguagens de programação.
Para a gramática de expressões, removendo a recursividade à esquerda removemos
também a ambigüidade. Segue abaixo uma nova gramática equivalente à gramática
anterior para as expressões aritméticas.
1
2
3
4
E
T
F
F
-->
-->
-->
-->
T+E | T-E | T
F*T | F/T | F
( E )
1|2| ...
%% recursiva à direita
Esta nova versão recursiva à direita está livre dos problemas citados, veja abaixo
a única árvore sintática possı́vel de ser construı́da para a expressão: 1+2*3.
1
2
3
4
5
6
7
8
9
10
11
E
/|\
T + E
/
/|\
F
F * E
|
|
|
1
2
T
|
F
|
3
Também foi resolvido o problema da precedência dos operadores. Foram introduzidos
diferentes nomes para cada nı́vel de precedência operadores: um T(ermo) é associado aos
operadores soma (+,-); um F(ator) é associado aos operadores multiplicação (*,/). Na
árvore construı́da o * tem precedência sobre o +, será executado antes.
Quanto à recursividade podemos escolher se queremos uma versão recursiva à esquerda
ou recursiva à direita. Segue a versão recursiva à direita.
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
1
2
3
4
E
T
F
F
-->
-->
-->
-->
E+T | E-T | T
T*F | T/F | F
( E )
1|2| ...
30
%% recursiva à esquerda
A recursividade à esquerda ou à direita determina a associatividade dos operadores.
Abaixo examinamos a expressão 1+2+3: recursiva à esquerda e à direita.
1
2
3
4
5
6
7
8
9
10
11
12
13
E
/|\
T + E
+
/
/|\
/ \
F
T + E
1
+
|
|
|
/ \
1
F
T
2
3
|
|
2
F
(1+(2+3))
|
3
(a) Associativa à direita
Gramática recursiva à direita
E
/|\
E + T
+
/|\
\
/ \
E + T
F
+
3
|
|
|
/ \
T
F
3
1
2
|
|
F
2
((1+2)+3)
|
1
(b) Associativa à esquerda
Gramática recursiva à esquerda
As árvores sintáticas das versões não ambı́guas da gramática apresentam vários nı́veis
de profundidade a mais que a versão ambı́gua.
Exercı́cio 2.2.5 Como removemos a recursividade à esquerda? (2 linhas)
Exercı́cio 2.2.6 Sempre é possı́vel remover a recursividade à esquerda?
Exercı́cio 2.2.7 Quando uma gramática é ambı́gua?
Exercı́cio 2.2.8 Sempre é possı́vel remover a ambigüidade?
2.2.3
Fatoração
As versões não ambı́guas da gramática E ainda não estão fatoradas. Segue uma versão
fatorada.
1
2
3
4
5
6
E --> T Tr
Tr --> +E | -E | []
T --> F Fr
Fr --> *T | /T | []
F --> ( E )
F --> 1|2| ...
%% recursiva à direita
%% e fatorada
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
31
O processo de fatoração introduziu mais dois sı́mbolos novos Tr e Fr (resto do Termo e
resto do Fator). Estes dois sı́mbolos não terminais aumentam ainda mais a profundidade
das árvores sintáticas.
Exercı́cio 2.2.9 Construa uma árvore sintática usando a gramática fatorada para a expressão 1+(2*3).
Temos um resultado associado ao problema da fatoração, que diz que podemos eliminar (ou acrescentar) produções vazias numa gramática.
Resultado 2.2.8 Para cada gramática LL(k) com produções vazias existe uma gramática
LL(k+1) sem produções vazias (e vice-versa). As gramáticas são equivalentes exceto que
a livre de produções vazias não pode gerar a sentença vazia.
Exercı́cio 2.2.10 Reescreva a gramática abaixo sem produções vazias (não poderá mais
gerar a sentença vazia). Esta gramática passara de LL(1) para LL(2).
1
2
A --> a A
A --> []
Solução:
1
2
A --> a A
A --> a
Note que agora não temos mais a geração da palavra vazia. Para processar esta
gramática não fatorada (sem produções vazias) é necessário olhar um sı́mbolo a mais à
frente na fita de entrada.
Exercı́cio 2.2.11 Sempre podemos escrever uma gramática sem produções vazias?
Exercı́cio 2.2.12 Para que serve a fatoração?
Exercı́cio 2.2.13 O que é precedência e associatividade de operadores? Qual a relação
destes conceitos com uma gramática que descreve uma linguagem de operadores e operandos? (3 linhas)
2.2.4
Análise sintática descendente
Na análise sintática descendente utiliza-se uma gramática LL(k), que não pode ser
recursiva à esquerda.
1
2
3
4
E
T
F
F
-->
-->
-->
-->
T+E | T-E | T
F*T | F/T | F
( E )
1|2| ...
%% recursiva à direita
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
32
A análise descendente, é baseada num autômato de pilha. Parte-se empilhando o
sı́mbolo inicial da gramática, e segue-se com uma seqüência de ações (uma para cada
transição):
• derivação (reescrita) usando uma produção – se no topo da pilha tem um
não terminal, procura-se uma produção para reescreve-lo; observa-se (k) tokens na
fita da entrada para escolher uma das produções;
• consumo de token – se no topo temos um terminal, que é o mesmo da fita de
entrada, então avançamos o ponteiro da fita de entrada e desempilhamos o terminal;
• parar – se a pilha está vazia, para-se e verifica-se se toda a fita foi consumida; se
sim a sentença foi reconhecida.
Se não temos nenhum destes três estados então temos uma situação de erro. Segue
um exemplo de análise sintática para a expressão 1+2*3. Foram usados (a-o) passos,
terminando com sucesso.
entrada: pilha: ação:
.1+2*3@
E (sı́mbolo inicial)
.1+2*3@
T+E E-->T+E deriva
.1+2*3@
F+E T-->F deriva
.1+2*3@
1+E F-->1 deriva
1.+2*3@
+E (consome 1)
1+.2*3@
E (consome +)
1+.2*3@
F*E E-->F*E deriva
1+.2*3@
2*E F-->2 deriva
1+2.*3@
*E (consome 2)
1+2*.3@
E (consome *)
1+2*.3@
T E-->T deriva
1+2*.3@
F T-->F deriva
1+2*.3@
3 F-->3 deriva
1+2*3.@
[] (consome o 3)
1+2*3.@
[] (para)
passo:
(a
(b
(c
(d
(e
(f
(g
(h
(i
(j
(k
(l
(m
(n
(o
Na escolha de cada produção a ser usada, temos que olhar (k) sı́mbolos da fita de
entrada. Por exemplo, no passo (b) temos um E no topo da pilha e temos três alternativas
para a produção E->T+E|T-E|T. Como saber qual das três devemos usar? Se olhamos dois
sı́mbolos na fita de entrada vemos um +, assim devemos escolher a produção E-->T+E.
Portanto, nesta gramática temos que usar um k=2. Se trabalharmos com a versão fatorada
desta gramática teremos um k=1.
Abaixo temos uma representação da análise sintática a partir dos passos usados na
construção da árvore sintática. A árvore é construı́da de cima para baixo da esquerda
para a direita. A fronteira da árvore representa a sentença sendo analisada; como uma
fita ela vai sendo consumida da esquerda para a direita.
33
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
1
2
3
E
/|\
T + E
E
/|\
T + E
/
4
F
5
6
7
E
/|\
T + E
/
F
|
1
E
/|\
T + E
/
/|\
F
F * E
|
1
E
/|\
T + E
/
/|\
F
F * E
|
|
1
2
8
9
10
.1+2*3@
a,b)E-->T+E
.1+2*3@
c)T-->F
1+2*3@
d)F-->1
1+.2*3@
e,f)
1+.2*3@
g)E-->F*E
1+.2*3@
h)F-->2
11
E
/|\
T + E
/
/|\
F
F * E
|
|
|
1
2
T
12
13
14
15
16
17
18
19
20
E
/|\
T + E
/
/|\
F
F * E
|
|
|
1
2
T
|
F
21
22
23
24
1+2*.3@
i,j)
1+2*.3@
k)E-->T
1+2*.3@
l)T-->F
E
/|\
T + E
/
/|\
F
F * E
|
|
|
1
2
T
|
F
|
3
1+2*.3@
1+2*3.@
m)F-->3
n)
Compare a seqüência de passos no autômato de pilha com a construção da árvore
sintática da gramática LL (Left to right, Left most derivation). Algumas transições no
autômato não modificam a árvore: quando no topo da pilha temos um terminal, ele é
apenas consumido.
A programação do reconhecedor como autômato de pilha é bem econômica, pois não
guarda a árvore sintática, mas apenas pequenos pedaços relativos à forma sentencial que
vem sendo processada.
2.2.5
Análise sintática ascendente
A análise ascendente faz uso de gramáticas LR(k), sem recursividade à direita. Este
método de análise ascendente compreende certas etapas complexas, como a geração de
tabelas para definir as transições do autômato de pilha. Não será apresentada aqui
a construção destas tabelas. Detalhes deste método devem ser buscados na literatura
especializada, listada na bibliografia.
Vamos apenas ilustrar o funcionamento do método para um autômato de pilha com o
objetivo de entendermos intuitivamente como funciona a análise ascendente. Será usada
a versão da gramática que segue.
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
1
2
3
E --> E+T | T
T --> T*F | F
F --> 1|2|3
34
%% recursiva à esquerda
Este método faz uso de um autômato de pilha com os três tipos de ações que seguem:
1. reduzir usando uma produção – operação contrária a uma derivação; ocorre no
topo da pilha: substituir o corpo de uma produção pelo não terminal;
2. consumir (empilhar) token - avançar o ponteiro da fita, empilhando o token;
3. parar – se a fita está vazia, parar e verificar se na pilha está só o sı́mbolo inicial;
neste caso a sentença foi reconhecida.
Para decidir qual a ação a ser executada num próximo passo, em termos do topo da
pilha e token na fita de entrada, é necessária a tabela auxiliar, que para a gramática do
exemplo é definida abaixo.
fita:
topo pilha:
ação:
+
F|T
reduzir
*
F
reduzir
fita vazia
F|T
reduzir
qualquer terminal
1|2|3
reduzir
qualquer terminal
+|*
consumir
+
E
consumir
*
T
consumir
fita vazia
E
parar
Na tabela abaixo listamos os passos necessários para reconhecer a sentença 1+2*3. No
inı́cio do processamento empilha-se o primeiro token da fita de entrada; ainda no passo
(a) consulta-se a tabela para saber qual é a ação do passo (b); pilha=1 para qualquer
valor da fita a ação é reduzir; utilizando-se a produção F-->1. Nos passos (b-d) acontece
a construção, de baixo para cima, do primeiro galho da árvore sintática para o token 1;
parte-se do token 1 e chega-se ao não terminal E: E-->T-->F-->1.
No passo (e) consultado a tabela, com fita=2 e pilha=+ que resulta na ação de consumir o 2 no passo (f); sempre o token consumido é empilhado. Este processo segue até
que a fita fica vazia e não tem mais reduções a serem feitas; neste caso, se na pilha temos
o sı́mbolo inicial então a sentença foi reconhecida, caso contrário é uma situação de erro.
Como mostram os passos (m-n) à sentença 1+2*3 foi reconhecida.
Se construirmos uma árvore seguindo os passos descritos na tabela abaixo, podemos
verificar que tentamos substituir sempre o não terminal mais à direita da subárvore sendo
construı́da, daı́ o nome LR (Left to right, Right most derivation).
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
entrada:
1.+2*3@
1.+2*3@
1.+2*3@
1.+2*3@
1+.2*3@
1+2.*3@
1+2.*3@
1+2.*3@
1+2*.3@
1+2*3.@
1+2*3.@
1+2*3.@
1+2*3.@
1+2*3.@
pilha:
1.
F.
T.
E.
E+.
E+2.
E+F.
E+T.
E+T*.
E+T*3.
E+T*F.
E+T.
E.
ação:
consumir 1
F-->1
T-->F
E-->T
(consumir +)
(consumir 2)
F-->2
T-->F
(consumir *)
(consumir 3)
F-->3
T-->T*F
E-->E+T
(sucesso)
35
passo:
(a
(b
(c
(d
(e
(f
(g
(h
(i
(j
(k
(l
(m
(n
Nós não apresentaremos métodos de programação para gramáticas LR(k). Uma
gramática LR(k) tem que ser implementada como um autômato de pilha associado a
uma ou mais tabelas para codificar as transições. Neste texto não abordamos métodos
baseados em tabelas, pois é difı́cil associar especificações de semântica com transições codificadas em tabelas. Caso for necessário indicamos utilizar uma ferramenta que permite
gerar o analisador diretamente a partir de uma especificação gramatical.
Exercı́cio 2.2.14 Quais as duas estratégias de análise sintática? Qual a relação delas
com os tipos de GLCs?
Exercı́cio 2.2.15 Como é executa a análise sintática descendente para a sentença ”aabb”
para a gramática L-> a L b | a b ? Mostre todos os passos?
Exercı́cio 2.2.16 Qual a diferença principal entre a análise sintática ascendente e descendente: em termos da máquina de pilha? e em termos da construção da árvore?
2.3
Gramáticas de Atributos
Desde o trabalho de Knuth (1968), que formalizou as GAs, inúmeros novos trabalhos
foram publicados sobre novas subclasses das GAs. Aqui nós mencionamos apenas duas
subclasses de GAs.
Inicialmente devemos saber que as DCGs implementam uma subclasse bem geral de
GAs e que em linguagens imperativas podemos codificar GAs com um poder suficiente
para implementar processadores de linguagens de programação (ou de linguagens naturais). A grande diferença entre uma GA e uma DCG é que na primeira as equações
semânticas são avaliadas como atribuições, enquanto que na segunda elas são avaliadas
com unificação (que é bi-direcional - uma atribuição nos dois sentidos).
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
36
Resultado 2.3.1 Uma GA com apenas atributos sintetizados S-GA tem o poder computacional de uma máquina de Turing.
Este resultado pode ser interpretado de várias formas. Sabemos que uma gramática
irrestrita, na classificação de Chomsky, também equivale a uma máquina de Turing.
Portanto uma GA tem o poder expressivo para especificar qualquer gramática irrestrita.
A classe de GAs com atributos herdados e sintetizados é mais geral que a classe
que contem somente atributos sintetizados. No entanto, noutra perspectiva, tudo o que
queremos computar pode ser computado por uma máquina de Turing. Então dada uma
GA com atributos herdados e sintetizados, definindo uma computação com valor prático,
podemos encontrar uma GA só com atributos do tipo sintetizado que processa a mesma
computação. Na pratica este resultado não é tão útil porque podemos programar GAs
com atributos herdados e sintetizados em qualquer linguagem de programação moderna,
como veremos mais adiante.
Atributos herdados e sintetizados
Uma GA estende uma gramática livre de contexto associando atributos aos sı́mbolos
não terminais da gramática, que em DCG são os predicados.
Resultado 2.3.2 Uma DCG com parâmetros equivale a uma GA.
1
2
3
%% GLC
A --> a A
A --> []
B --> b B
B --> []
4
5
6
7
%%DCG
a(N+1) -->[a], a(N).
a( 0 ) -->[].
%% S-GA em DCG
a(M) --> [a], a(N), {M := N+1}.
a(M) --> [], {M := 0}.
Acima temos um S-GA para contar o número de as da fita de entrada usando apenas
um atributo do tipo sintetizado. Esta versão usa o atributo M, que recebe o valor de
N+1. Note que os atributos são passados do corpo (RHS) para a cabeça das produções
(LHS). Na árvore sintática decorada com os atributos, vemos que estes atributos sobem
pela estrutura da árvore, por isso são chamados de sintetizados, ver Figura 2.7.
1
2
3
4
5
%% DCG
b0( M )--> b(0,M).
b(AC,M )--> [b], b(AC+1,M).
b(AC,AC)--> [].
%% AC é um ACumulador
%% L-GA em DCG
b0( M )--> b(0,M).
b(AC,M )--> [b], {AC1:=AC+1}, b(AC1,M).
b(AC,AC)--> [].
Já definimos a subclasse S-GA, onde temos somente atributos do tipo sintetizado.
Outra subclasse importante é a L-GA (Left to right transversal GA). Uma gramática
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
37
Figura 2.7: Árvore de uma sentença ”aaa”, para uma gramática S-GA, com atributos só sintetizados (sobem) para contar os (a)s.
de atributos é L-GA se suas equações podem ser calculadas durante a análise sintática
LL(k). Ou também durante um caminho em profundidade da esquerda para a direita.
Acima mostramos um exemplo. Para este exemplo, os atributos são melhor visualizados
em uma árvore sintática da gramática: um atributo herdado desce pela árvore e um
sintetizado sobe, ver Figura 2.8. Nesta L-GA temos atributos sintetizados e herdados. O
sintetizado é o M e os herdados são o AC e AC1.
EXEMPLO 2.3.1 (Gramática de atributos não L-GA)
Abaixo temos um exemplo de uma gramática que não é L-GA. Os atributos do corpo da
produção x devem ser calculados da direita para esquerda.
1
2
3
4
%% GLC
x --> a, b.
a --> [a],a | [].
b --> [b],b | [].
.
5
6
7
8
9
10
11
%% nao L-GA
x(N) --> {N := Na+3},a(Na,AC),{AC:=Nb+4},b(Nb).
a(N, AC) --> [a], a(N, AC+1).
a(AC,AC) --> [].
b(N+1 ) --> [b], b(N).
b(0
) --> [].
Numa linguagem imperativa a produção x codificada como um procedimento de uma
gramática do tipo LL(k) tem a forma ilustrada abaixo: um atributo herdado equivale
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
38
Figura 2.8: Árvore para a sentença ”bbb”, da gramática L-GA, com atributos herdados (descem)
e sintetizados (sobem), contando os (b)s.
a um parâmetro de entrada num procedimento (Ac :int); e, um atributo sintetizado
equivale a um parâmetro de saı́da (var Na:int), em Pascal. Se executarmos o corpo de
produção x da esquerda para a direita (no procedimento imperativo de cima para baixo)
não temos os valores nas variáveis para calcular as expressões para fazer as atribuições:
em {N := Na+3} ainda não temos o valor do Na que é retornado da procedure a. Isto
causa um erro de execução. O mesmo acontece com a atribuição AC:=Nb+4.
1
2
3
4
5
6
7
8
procedure x(var N:int);
begin
{N := Na+3};
a(Na,AC);
{AC:=Nb+4};
b(Nb);
end;
procedure a(var Na:int; Ac:int); begin ... end;
Conhecer a classe de um atributo auxilia na codificação das equações. Se o atributo
desce deve ser calculado antes de se chamar o predicado, no corpo da produção, mais à
esquerda da chamada. Se ele sobe deve ser calculado no final da regra, mais à direita.
Isto é necessário sempre que usamos uma atribuição, porque se uma das variáveis não
estiver instanciada ela falha.
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
39
Por outro lado, apesar da GA exemplificada acima não poder ser programado numa
linguagem imperativa ela pode ser programa em linguagens que permitem o uso de variáveis livres (sem conteúdo) em equações que são avaliadas por demanda. Um exemplo
de uma destas linguagens é o Prolog. Se o sistema ainda não conhece o valor da variável,
ele gera a expressão com a variável livre, que fica ”esperando” até o valor ser encontrado. Portanto, no Prolog, se usamos a unificação (=) uma equação pode ser colocada
em qualquer posição na regra. Abaixo reescrevemos a produção x, onde substituı́mos a
atribuição por uma igualdade (unificação). Isto é suficiente para esta regra ser executável
em Prolog. Portanto em DCG podemos escrever GAs de várias classes, inclusive algumas
não L-AG. Segue a L-AG em Prolog, que é executável: retorna o N que é o total do
número de as mais 3 e o número de bs mais 4.
1
2
3
4
5
6
7
8
x(N) --> {N = Na+3},a(Na,AC),{AC=Nb+4},b(Nb).
a(N, AC) --> [a],
a(N, AC+1). a(AC,AC) --> [].
b(N+1 ) --> [b], b(N).
b(0
) -->[].
%
%?- x(N, [a,b,b],[]).
%
N = 0+1+1+4+1+3
EXEMPLO 2.3.2 (Uma linguagem para robôs)
Exercı́cio 2.3.1 Faça uma DCG para uma linguagem de robôs, onde o robô pode se
mover apenas em quatro direções: traz, frente, esq, dir. Usando dois parâmetros gere
uma soma para cada um dos sentidos de deslocamento (traz/frente) e (esq/dir). Por
exemplo:
?-move(F,D,[esq,dir,esq,frente,frente,dir,dir,pare],[]).
F= 0+0+0+1+1+0+0+0,
D=-1+1-1+0+0+1+1+0 Yes
Solução:
1
2
3
4
5
6
7
move(0,0) --> [pare].
move(D,F) --> passo(D,F).
move(D+D1,F+F1)--> passo(D,F), move(D1,F1).
passo( 1, 0) --> [dir].
passo(-1, 0) --> [esq].
passo( 0, 1) --> [frente].
passo( 0,-1) --> [traz]
Exercı́cio 2.3.2 A gramática do robô aceita sentenças como [frente, frente, pare]
mas também [frente, frente]. Reescreva-a para que aceite somente sentenças terminadas por [pare].
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
40
Exercı́cio 2.3.3 Calcule os valores, para D, F, acrescentando uma regra cmove, que
chama move, sem modificar o move.
Exercı́cio 2.3.4 Acrescente a dimensão Z, desce/sobe. Pense num braço mecânico se
movimentando em 3D.
Exercı́cio 2.3.5 Com base nos exemplos de GA apresentados, faça um conjunto de
equações associadas à gramática abaixo de modo que retornem três informações: (1)
o número de palavras (P), (2) o número de vogais (V) e o (3) o número de espaços em
branco (B). Comece pela DCG dada abaixo.
1
2
3
4
5
6
7
lista --> palavra.
lista --> palavra, br, lista.
br
--> [32], br.
br
--> [32].
palavra --> letra, palavra.
palavra --> letra.
letra
--> [X], {64<X,X<91,!; 96<X,X<123,!}.
?-lista(P,V,B,"abc xyz
P=3, V=10, B=3
2.4
aaaeeeiii",[]).
Calculando o valor de um número binário
Uma abordagem para se calcular o valor decimal para um número binário é varrendo
a seqüência de dı́gitos da esquerda para a direita usando a fórmula que segue:
%%
%%
%%
%%
?-
N é o valor acumulado
B é um dı́gito binário
N := B+N*2
1101 = 13
n1(N,[1,1,0,1],_), X is N.
N = 1+2* (0+2* (1+2* (1+2*0)))
X = 13
Esta fórmula é codificada no programa que segue
n1(R)-->bs(0,R).
bs(N,R)-->b(B),{N1 = 2*N+B}, bs(N1,R).
bs(N,N)-->[].
b(0)
-->[0].
b(1)
-->[1].
Na notação original de Knuth escreve-se a gramática n1 como segue:
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
41
Figura 2.9: Árvore com atributos herdados (descem) e sintetizados (sobem).
n
bs1
bs
b
b
-->
-->
-->
-->
-->
bs
b bs2
[]
0
1
{bs.N
{bs2.N
{bs.R
{b.B
{b.B
:=
:=
:=
:=
:=
0; n.R := bs.R}
2*bs1.N+b.B; bs1.R := bs2.R}
bs.N}
0}
1}
Aqui os sı́mbolos não terminais das produções são indexados (bs1 bs2). No programa
Prolog as variáveis são indexadas (N N1). Estas duas variáveis implementam o mesmo
atributo N. Aqui vemos que o atributo N é associado ao não terminal bs2 que está no
corpo da regras, e que ele vem de bs1 que é seu pai.
O fluxo destes atributos é melhor ilustrado na Figura 2.9, onde podemos ver o valor
sendo calculado na descida; na subida o valor já calculado é passado de volta.
Exercı́cio 2.4.1 Uma segunda forma para calcular o valor decimal usa apenas atributos
sintetizados. Parte-se da direita para a esquerda, usando dois atributos, um armazena o
total, e a outro armazena a potência de 2 que é multiplicada pelo digito corrente, como
exemplificado abaixo. Programe-a?
1
2
3
4
5
6
%% N :=P*B+N
%% P :=P*2
%% 1101 = 13
?- n3(N,[1,1,0,1],_), X is N.
N = 1*2*2*2*1+ (1*2*2*1+ (1*2*0+ (1*1+0)))
X = 13
Solução: Esta segunda versão é codificada abaixo.
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
1
2
3
4
5
42
n3(N)-->bs3(_,N).
bs3(P1,N1)-->b(B),!,bs3(P,N),{P1 = P*2, N1 = P*B+N}.
bs3(1,0)-->[].
b(0)
-->[0].
b(1)
-->[1].
Exercı́cio 2.4.2 Faça uma GA para traduzir um número binário para hexadecimal e
vice-versa. Assuma as GLCs dadas abaixo; um hexadecimal equivale a quatro dı́gitos
binários.
1
2
3
h --> d h
h --> d
d --> [0]...[9]|[a]..[f]
4
hb --> b4 hb
hb --> b4
b4 --> b b b b
b --> [0] | [1]
Segue dois exemplos de como deve funcionar a gramática.
?- h(BIN,[f,0,2],[]).
BIN=[1,1,1,1, 0,0,0,0, 0,0,1,0]
?- hb(H, [1,1,1,1, 0,0,0,1], []).
H="F1"
EXEMPLO 2.4.1 ((*opcional) Calculando a parte fracionária)
Knuth (1968), para apresentar o formalismo GA, usou como exemplo uma gramática que
converte números binários em números decimais: ”1101.0100b = 13.25d”.
Para este mesmo problema, iniciamos apresentando uma gramática livre de contexto.
1
2
3
4
5
6
7
s -->n.
n -->bs,[’.’],bs.
n -->bs.
bs-->b,bs.
bs-->b.
b -->[0].
b -->[1].
Esta gramática pode ser testada com perguntas como:
?-n([1,0,’.’,1,0],L). L=[] Yes.
Nosso objetivo é calcular também os valores fracionários, como está ilustrado abaixo:
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
43
%% 1101.01 = 13.25
?-n(V,[1,1,0,1,’.’,0,1],L).
V = 13.25
L = []
?- n(V,[’.’,0,1,1,1,0],[]).
V = 0.4375
O predicado bs calcula a parte inteira. A idéia é usa-lo também para a parte decimal,
que é o valor inteiro dividido por (2 elevado ao número de dı́gitos): .01b = 1/22 = 0.25
ou .0100b = 4/28 = 0.25 . Para isso é necessário retornar, em bs2, também o número de
dı́gitos; o que é feito com a variável Ls. A solução é codificada no programa abaixo.
1
2
3
4
5
n(V) -->bs2(0,Vi,_),[’.’],!,bs2(0,Vd,L), {V is Vi+Vd/2**L}.
n(V) -->bs2(0,Vi,_),{V is Vi}.
bs2(N,R,Ls+1)-->b(B),!,bs2(N1,R,Ls),{N1 = B+2*N}.
bs2(N,N,0)-->[].
b(0) -->[0].
6
7
b(1) -->[1].
Exercı́cio 2.4.3 Para esta gramática n, desenhe a árvore sintática com atributos, similar
a Figura 2.9 para a sentença 101.11 que gera o valor 5.75.
Exercı́cio 2.4.4 Classifique todas as variáveis que aparecem no programa acima como
atributos herdados ou sintetizados?
2.5
Avaliar expressões aritméticas
Quando falamos sobre análise sintática apresentamos a gramática abaixo para expressões aritméticas.
1
2
3
4
E
T
F
F
-->
-->
-->
-->
T+E | T-E | T
F*T | F/T | F
( E )
1|2| ...
Com esta gramática podemos gerar árvores abstratas, similares com as árvores sintáticas,
que tratam da precedência dos operadores, onde as operações de menor precedência estão
no topo (ver abaixo).
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
44
+
/ \
1
*
/ \
2 3
No nı́vel semântico, podemos definir uma GA para avaliar estas expressões. Abaixo é
definida uma gramática de atributos, com equações semânticas para calcular o valor de
cada expressão da linguagem, por exemplo, calcular o valor de (20+4)*4/8 que é 12.
E1 -->
E1 -->
T1 -->
T1 -->
F -->
F -->
F -->
...
T+E2
T-E2
F*T2
F/T2
(E)
1
2
{E1 .val:=T.val+E2
{E1 .val:=T.val-E2
{T1 .val:=F.val*T2
{T1 .val:=F.val/T2
{F.val := E.val}
{F.val := 1}
{F.val := 2}
...
.val}
.val}
.val}
.val}
Esta gramática de atributos define uma semântica para o cálculo do valor das expressões geradas pela linguagem. Uma equação é definida para cada produção (unidade
sintática), por exemplo, a equação {F.val = 1} associada à produção F-->1 é lida como,
o atributo val do F recebe 1. De forma similar a equação {E1 .val=T.val+E2 .val} associada à produção E1 --> T+E2 é lida como o atributo E1 .val recebe a soma dos atributos
T.val e E2 .val. Note que aqui, pelas equações podemos identificar que o atributo val é
sintetizado, porque flui em direção pai (se for examinado numa árvore sintática).
Exercı́cio 2.5.1 Desenhe uma árvore sintática para a sentença 1+2*3, decorada com os
atributos, para a gramática de atributos, definida acima.
2.5.1
Programando a GLC como DCG
Segue a gramática livre de contexto, codificada em DCG, para a linguagem de expressões; usamos a codificação E=expr, T=Termo e F=Fator:
1
2
3
4
5
6
7
8
expr --> termo,[+],expr.
expr --> termo,[-],expr.
expr --> termo.
termo--> fator,[*],termo.
termo--> fator,[/],termo.
termo--> fator.
fator --> [X],{integer(X)}.
fator --> [’(’], expr, [’)’].
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
45
A produção fator --> [X],{integer(X)} define uma regra válida para todos os
inteiros da linguagem Prolog. O predicado integer/1 testa se uma constante é do tipo
inteiro como exemplificado:
?- integer(10). Yes
?- integer(a). No
A gramática com sı́mbolo inicial expr é um programa executável em Prolog. Podemos
perguntar se uma expressão é reconhecida por ela: ?- expr([1,+,2,*,3],X). X=[], Yes
2.5.2
Calculando o valor com equações semânticas
Abaixo apresentamos a versão em Prolog desta gramática, estendida com atributos e
equações semânticas que calculam o valor da expressão aritmética.
A sintaxe do Prolog difere um pouco da notação de GA: numa GA um atributo é
associado a um sı́mbolo não terminal. Dois sı́mbolos com mesmo nome numa produção
são indexados com um dı́gito, em Prolog este dı́gito indexador é associado a uma variável,
por exemplo E1. De qualquer modo, a semântica das equações é a mesma.
1
2
3
4
5
6
7
8
expr(E)-->
expr(E)-->
expr(T)-->
termo(T)-->
termo(T)-->
termo(F)-->
fator(X)-->
fator(E)-->
termo(T),[+],expr(E1),{E is T+E1}.
termo(T),[-],expr(E1),{E is T-E1}.
termo(T)
fator(F),[*],termo(T1),{T is F*T1}.
fator(F),[/],termo(T1),{T is F/T1}.
fator(F).
[X],{integer(X)}.
[’(’], expr(E), [’)’].
Seguem algumas perguntas onde é retornado o valor da expressão.
?-expr(V,[1,+,2,*, 3],X).
V=7, X=[]
?- expr(V,[1,+,2,*, ’(’,3,+,1, ’)’],X).
V=9, X=[],
?- expr(V,[’(’,20,+,4,’)’, *, 4, /, 8],X).
V=12, X=[]
Exercı́cio 2.5.2 O que é um atributo herdado e o que é um atributo sintetizado? Como
podemos diferenciar eles só olhando para as produções?
Exercı́cio 2.5.3 Qual a diferença entre as classes de GAs: S-GA e L-GA?
Exercı́cio 2.5.4 Reescreva a solução do exercı́cio sobre a linguagem para robôs na notação de GA original de Knuth?
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
2.5.3
46
O problema da associatividade à esquerda para LL(k)
Esta implementação apresenta um problema de associatividade. Este problema aparece em operações não associativas, por exemplo, em seqüências de divisões e em seqüências
de somas e/ou subtrações, como vemos abaixo.
?- expr(V,[1,/,2,/,4,*,3],[]), display(V), X is V.
/(1, /(2, *(4, 3)))
X = 6
?- expr(V,[1,-,2,+,4,-,3],[]), display(V), X is V.
-(1, +(2, -(4, 3)))
X = -2
A primeira expressão 1/2/4*3 foi parentizada como (1/(2/(4*3)))=6; o certo é
parentiza-la como (((1/2)/4)*3)=0.375. Este problema de associatividade acontece
com gramáticas recursivas à direita, as LL(k). Estas gramáticas geram (naturalmente)
árvores abstratas associativas à direita. Mas este problema tem solução: isto é, numa
gramática do tipo LL(k) podemos gerar uma árvore para avaliar uma expressão com
operadores associativos à esquerda. Segue o esboço de uma solução, aqui exemplificada,
para o operador +. Esta mesma solução deve ser adotada para os outros operadores.
1
2
3
4
eLeft(
To
)--> tLeft(T),[+], eLeft(
T/To).
eLeft(Ti/To
)--> tLeft(T),[+], eLeft((Ti+T)/To).
eLeft(Ti/(Ti+T))--> tLeft(T).
tLeft(X)--> [X],{integer(X)}.
5
A idéia é, em cada produção, passar um termo Ti adiante, para ser parentizado junto
com seu irmão imediato à direita. Este termo Ti é um atributo herdado. O resultado em
cada produção é uma expressão parentizada que é retornada pelo atributo sintetizado To
ou (Ti+T).
Segue alguns exemplos de execução, onde mostramos a parentização correta.
?- eLeft(0/V,[1,+,2,+,4,+,3],[]), display(V).
+(+(+(+(0, 1), 2), 4), 3)
Yes
?- eLeft(V,[1,+,2,+,4,+,3],[]), display(V).
+(+(+(1, 2), 4), 3)
Yes
Devemos agora incluir na gramática das expressões o processo descrito acima ( a
solução para a associatividade à esquerda). Para isso devemos tratar dois problemas: (1)
quando temos várias operações temos que passar junto qual a operação que está associada
47
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
ao atributo herdado; e, (2) devemos formar as diferentes combinações para cada regra
(por exemplo, [Ti,+] [Ti,-]). Para denotar que vamos iniciar uma nova expressão ou
um novo termo, usamos prefixo x().
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xexpr(
x(E))--> xtermo(x(T)),[+],xexpr([
T ,+]/Eo),{E =
xexpr(
x(E))--> xtermo(x(T)),[-],xexpr([
T ,-]/Eo),{E =
xexpr(
x(T))--> xtermo(x(T)).
xexpr([Ti,+]/E)--> xtermo(x(T)),[+],xexpr([(Ti+T),+]/Eo),{E =
xexpr([Ti,+]/E)--> xtermo(x(T)),[-],xexpr([(Ti+T),-]/Eo),{E =
xexpr([Ti,-]/E)--> xtermo(x(T)),[+],xexpr([(Ti-T),+]/Eo),{E =
xexpr([Ti,-]/E)--> xtermo(x(T)),[-],xexpr([(Ti-T),-]/Eo),{E =
xexpr([Ti,+]/(Ti+T))--> xtermo(x(T)).
xexpr([Ti,-]/(Ti-T))--> xtermo(x(T)).
%%
xtermo( x(T)
)--> xfator(F),[*],xtermo([F,*]/Ti),{T = Ti}.
xtermo( x(T)
)--> xfator(F),[/],xtermo([F,/]/Ti),{T = Ti}.
xtermo( x(F)
)--> xfator(F).
xtermo([Fi,*]/T)--> xfator(F),[*],xtermo([(Fi*F),*] /Ti),{T =
xtermo([Fi,*]/T)--> xfator(F),[/],xtermo([(Fi*F),/] /Ti),{T =
xtermo([Fi,/]/T)--> xfator(F),[*],xtermo([(Fi/F),*] /Ti),{T =
xtermo([Fi,/]/T)--> xfator(F),[/],xtermo([(Fi/F),/] /Ti),{T =
xtermo([Fi,*]/(Fi*F))--> xfator(F).
xtermo([Fi,/]/(Fi/F))--> xfator(F).
%%
xfator(X)--> [X],{integer(X)}.
xfator(E)--> [’(’], xexpr(x(E)), [’)’].
Eo}.
Eo}.
Eo}.
Eo}.
Eo}.
Eo}.
Ti}.
Ti}.
Ti}.
Ti}.
Segue uma lista de testes para a gramática com o problema da associatividade resolvido. Aqui podemos combinar diferentes operadores com ou sem parênteses.
?-xexpr(x(V),[1,/,2,/,4,*,3],[]), display(V), X is V.
*(/(/(1, 2), 4), 3)
X = 0.375
?-xexpr(x(V),[1,+,2,*, ’(’,3,+,1, ’)’],[]), display(V), X is V.
+(1, *(2, +(3, 1)))
X = 9
?- xexpr(x(V),[1,+,2,*,3,+,1],[]), display(V), X is V.
+(+(1, *(2, 3)), 1)
X = 8
?- xexpr(x(V),[1,-,2,*,3,+,1],[]), display(V), X is V.
+(-(1, *(2, 3)), 1)
X = -4
?- xexpr(x(V),[1,-,2,+,4,-,3],[]), display(V), X is V.
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
48
-(+(-(1, 2), 4), 3)
X = 0
A gramática não esta calculando o valor das subexpressões – somente montamos a
árvore sintática parentizada. No final o termo é calculado com o comando de atribuição
is. Podemos substituir a unificação por atribuição no final de cada produção para calcular
as subexpressões.
O número de regras da gramática podem ser reduzido se usamos outros recursos do
Prolog. A regra abaixo equivale a quatro regras da gramática acima, além de ser mais
eficiente, pois não precisa tentar todas as combinações.
1
2
3
xexpr([Ti,Oi]/E)--> xtermo(x(T)),[O],{O=’+’;O=’-’},
{TiOiT =..[Oi,Ti,T]}
xexpr([TiOiT,O]/Eo),{E = Eo}.
Exercı́cio 2.5.5 PROJETO: Reescreva a gramática dada acima, com as sugestões dadas
na produção acima, usando disjunções (;) e o operador (=..).
2.5.4
Gerando notação polonesa com ações semânticas
Abaixo segue outra versão da gramática de expressões com ações semânticas de
escrita: escreve-se na saı́da o código em notação polonesa para a expressão.
Normalmente diferenciamos equações semânticas de ações semânticas. Equações definem relações entre atributos, locais a uma regra gramatical, são mais formais e mais
declarativas; como as usadas na gramática de atributos para cálculo do valor da expressão.
Por outro lado, as ações semânticas são mais procedurais, tipicamente envolvem entrada
e saı́da. Assim elas possuem efeito colateral, uma vez que escrevemos um valor a escrita
não pode ser desfeita. Portanto, programas com ações semânticas necessariamente devem
ser fatorados para não se ter retrocesso. Por causa disso, fatoramos as produções da
gramática de expressões: cada produção começa com a parte comum (termo), seguida
de uma produção com várias alternativas para a parte diferenciada (rtermo) – resto do
termo.
1
2
3
4
5
6
7
8
9
10
expr --> termo,rexpr.
rexpr --> [+],expr, {write(some),nl}.
rexpr --> [-],expr, {write(subt),nl}.
rexpr --> [].
termo--> fator,rtermo.
rtermo--> [*],termo, {write(mult),nl}.
rtermo--> [/],termo, {write(divi),nl}.
rtermo--> [].
fator --> [X],{integer(X)},{write(X), write(’ enter’), nl}.
fator --> [’(’], expr, [’)’].
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
49
O efeito das ações semânticas é escrever uma seqüência de passos a serem executados
numa calculadora do tipo HP para se calcular a expressão. Esta notação para representar
expressões sem parênteses é também chamada de notação polonesa. Como segue:
?- expr([10,+,20,*,33],[]).
10 enter
20 enter
33 enter
mult
some
?- expr([1,-,2,+,4,-,3],[]).
1 enter
2 enter
4 enter
3 enter
subt
some
subt
Exercı́cio 2.5.6 Faça uma árvore sintática decorada com as ações semânticas, para a
gramática versão fatorada que gera a notação polonesa, para a sentença 1+2*3.
Exercı́cio 2.5.7 Qual a diferença entre uma equação semântica e uma ação semântica?
Exercı́cio 2.5.8 PROJETO: A gramática que gera notação polonesa não é associativa à
esquerda. Reveja a solução proposta acima para parentizar uma expressão com associatividade à esquerda e utilize o método para fazer a geração do código em notação polonesa
da forma correta.
Exercı́cio 2.5.9 PROJETO: Fatore a versão da gramática que calcula o valor da expressão, com o problema da associatividade resolvido. Note que fatorar uma gramática
de atributos, implica na rescrita das equações semânticas.
Exercı́cio 2.5.10 PROJETO: Abaixo temos uma gramática para expressões booleanas.
Definimos uma ordem de precedência (maior) - ^ v -> = (menor).
Para avaliarmos uma expressão corretamente devemos também trabalhar com a associatividade à esquerda. Implemente uma DCG para parentizar expressões booleanas,
considerando à associatividade à esquerda.
1
2
3
4
5
E4
E3
E2
E1
E0
-->
-->
-->
-->
-->
t | f | Q ... | (-E0)
E4 ^ E3 | E4
E3 v E2 | E3
E2 -> E1 |E2
E1 = E0 | E1
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
2.6
50
Regras gramaticais reversı́veis: geração x reconhecimento
Regras DCG podem ou não ser reversı́veis. Abaixo apresentamos um programa que
traduz uma lista de dı́gitos no valor por extenso e vice versa. Por exemplo, se perguntarmos quanto é por extenso o valor ”123” o sistema responde ”cento e vinte e três”. E, se
perguntarmos qual é o valor para ”cento e vinte e três”, ele responde 123. Portanto, esta
versão da gramática pode ser utilizada tanto para reconhecimento como a geração;
de valores ou de valores por extenso.
ddd(C,[1,2,3],[]).
C = [cento, e, vinte, e, tres]
Yes
?- ddd([cento, e, trinta, e, um],V,[]).
V = [1, 3, 1]
Yes
Nesta gramática DCG o número e a sentença gerada são representados por listas;
numa implementação, numa linguagem imperativas podem ser representados por string
de caracteres.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
d([um])-->[1].
d([dois])-->[2].
d([tres])-->[3].
%%...
dd([dez])-->[1,0].
dd([onze])-->[1,1].
%%...
dd([vinte])-->[2,0].
dd([vinte,e|D])-->[2],d(D).
dd([trinta])-->[3,0].
dd([trinta,e|D] )-->[3],d(D).
%%...
ddd([cem])-->[1,0,0].
ddd([cento,e|DD])-->[1],dd(DD).
Esta gramática codifica um programa reversı́vel, em DCG. Para uma gramática DCG
ser reversı́vel deve satisfazer três requisitos:
• não usar funções aritméticas, nem operadores de corte; ou outras construções com
efeito colateral;
• toda regra deve consumir algum token; quando usada nos dois sentidos;
• não deve ter produções vazias (similar ao anterior).
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
51
O programa do exemplo, consome tokens nos dois sentidos: quando gera um valor consome as palavras; e, quando gera as palavras consome os dı́gitos.
O programa que segue, quando reconhece uma lista de z(s), consome letras
?- z([z,z,a],X), X=[a]; Mas quando é usado ao contrário ?-z(X,[]) ele não consome
nada, assim sendo, o resultado é um erro de execução.
1
2
z -> [z], z.
z -> [].
?- z([z,z,a],X).
X = [a] Yes
?- z(X,[]).
ERROR: Out of local stack
Exception: (31,742) z(_G95224, []) ?
Exercı́cio 2.6.1 Qual a diferença entre geração e reconhecimento?
Exercı́cio 2.6.2 Dê um exemplo de uma gramática reversı́vel em DCG, diferente exemplificada, e que funcione?
EXEMPLO 2.6.1 (Outro exemplo de geração)
Vimos uma gramática para a linguagem an bm cn dm que apenas testava se uma dada
sentença pertencia a sua linguagem. Se, a partir daquela versão, modificamos a produção
inicial s como segue abaixo, podemos também utiliza-la para gerar todas as sentenças
válidas, para os valores de M e N variando de 1 até 3.
1
2
3
4
5
6
7
8
9
10
s --> {X=[1,1+1,1+1+1],member(N,X),member(M,X)},
a(N),b(M),c(N),d(M).
a(N+1)-->[a],a(N).
a( 1)-->[a].
b(N+1)-->[b],b(N).
b( 1)-->[b].
c(N+1)-->[c],c(N).
c( 1)-->[c].
d(N+1)-->[d],d(N).
d( 1)-->[d].
Aqui o X é uma lista com o conjunto dos valores 1, 2, 3 e a função member é usada para
selecionar um dos valores do conjunto para M e N. Com isso, quando a(N) é executado ele
traz uma seqüência de as de comprimento N.
CAPÍTULO 2. FUNDAMENTOS PARA GRS, GLCS E GAS
?- s(X,[]).
X = [a, b, c,
X = [a, b, b,
X = [a, b, b,
X = [a, a, b,
X = [a, a, b,
X = [a, a, b,
d]
c,
b,
c,
b,
b,
;
d,
c,
c,
c,
b,
d]
d,
d]
c,
c,
52
;
d, d] ;
;
d, d] ;
c, d, d|...] ;
Exercı́cio 2.6.3 Modifique a gramática para que seja feito o cálculo dos valores para M e
N. Assim X=[1,2,...]. Pense somente numa gramática para geração, pois, os operadores
aritméticos não são reversı́veis. Acrescente no final de cada produção recursiva uma
ação semântica tipo {N is N1-1}. Nesta nova versão é necessário também acrescentar
operadores de corte, visando tornar o programa determinı́stico.
EXEMPLO 2.6.2 ((*opcional)Regras gramaticais vs cláusulas Prolog)
As regras DCGs (escritas com o sı́mbolo gramatical -->) são automaticamente traduzidas para cláusulas Prolog. Cada regra equivale a uma cláusula estendida com dois
argumentos. Com o comando listing podemos ver como o Prolog traduz as regras
DCG para cláusulas (OBS: diferentes sistemas Prolog, geram este código com pequenas
variações).
?-listing([r,a,b]).
r(A, B) :- a(A, C),b(C, B).
a(A, B) :-’C’(A, a, C),a(C, B).
a(A, A).
b(A, B) :-’C’(A, b, C),b(C, B).
b(A, A).
%%
%%
%%
%%
%%
r
a
a
b
b
-->
-->
-->
-->
-->
a, b.
[a],a.
[].
[b],b.
[].
O predicado ’C’/2 simplesmente extrai (ou come) da lista de entrada um terminal.
Abaixo temos a sua definição. Este predicado nas primeiras implementações do Prolog
Edimburgo era chamado de ”connectors”, daı́ o ’C’/2.
?-listing(’C’).
’C’([A|B], A, B).
Exercı́cio 2.6.4 Teste o seu sistema Prolog. Consulte o programa da gramática r, acima. Depois liste as cláusulas e compare o código com o código apresentado acima.
Exercı́cio 2.6.5 Mostre a equivalência entre as cláusulas s1 e s2 abaixo, que traduzem
a regra s1 -->[a] para cláusulas. Usando substituições, reescreva s2 até chegar em s1?
1
2
3
s1([a|A], A).
s2(A, B) :- ’C’(A, a, B).
’C’([A|B], A, B).
Capı́tulo 3
Técnicas para Programação de
Gramáticas
Métodos de programação de gramáticas
Aqui apresentamos a codificação de algumas mini-gramáticas, visando introduzir os
métodos básicos sobre programação de gramáticas em linguagens imperativas. Gramáticas
são formalismos descritos por produções. Devemos aprender, num primeiro momento, a
usar um método para programar cada produção; para num segundo momento aplica-lo
para a gramática toda. Nosso objetivo é que cada produção de uma gramática resulte numa linha(s) de código de um programa que implementa a gramática.
Com isso podemos facilmente alterar a especificação do problema (a gramática) e em
seguida modificar o código da implementação.
Um método pode ser
aplicado a qualquer gramática desde que satisfeitas as restrições que são imposta para o seu uso. Inicialmente trabalharemos com pequenas gramáticas ilustrativas. Nos
próximos capı́tulos, usamos estes métodos em estudos de casos de linguagens de programação mais realistas (subconjuntos de) e até de linguagens naturais (subconjuntos
de).
Mostraremos como programar os três tipos de linguagens formais (regulares, livres
e sensı́veis ao contexto). Os exemplos são ilustrados inicialmente na linguagem Pascal.
Nos próximos capı́tulos, estudos de casos utilizando os métodos expostos são também
apresentados para as linguagens C++ e Java.
NOTA: Nos códigos fontes apresentados nesta seção, em linguagens imperativas,
utilizamos uma técnica de avaliação de expressões lógicas que se chama short-circuit
(curto-circuito). Na linguagem Pascal, dependendo do compilador, é necessário informar
com uma diretiva de compilação dizendo que queremos esta forma de avaliar expressões.
Nas linguagens C, C++ e Java, os operandos AND e OR comuns (&& e ||) são avaliadas
em curto-circuito.
53
CAPÍTULO 3. TÉCNICAS PARA PROGRAMAÇÃO DE GRAMÁTICAS
3.1
54
Medidas de tempo
Abaixo temos uma tabela que compara os tempos de diferentes métodos de codificação
de gramáticas em diferentes linguagens. No testes do tempo são utilizadas fitas de entrada
com comprimento de 100 palavras. E, a fim de se obter tempos significativos a execução
é repetida 10 mil vezes. Os tempos foram medidos em milésimos de segundos.
Na última coluna temos dois tempos para a linguagem C, no primeiro é utilizada uma
função para avançar o ponteiro na fita de entrada, enquanto que na segunda, a codificação
usa macros (#define) para melhorar a eficiência.
Gramática Método
Pascal Java C++
Regular
Autômato com goto
205
54
Iterativo
220
1780
60
Recursivo
350
1920
110
Livre
DRSR
330
1950
55
DRCR salvando o próximo
495
3650
105
DRCR com costura
520
4845
169
Atributos DRCR com costura
600
2080
168
DRSR = Descendente recursivo sem retrocesso
DRCR = Descendente recursivo com retrocesso
#define
50
55
68
50
60
114
115
Comparando-se as diferentes linguagens vemos que a linguagem C(C++) é a mais
eficiente para a codificação de gramáticas. Em segundo lugar esta o Pascal (4x mais
lento) e por fim Java. Pelos testes Java é 20 a 30 vezes mais lento.
Nas gramáticas regulares o método autômato com GOTO é o mais eficiente, pois
não utiliza chamadas para funções. O método recursivo é mais lento devido a sucessivas
chamadas de funções realizadas durante o processo; porém, não é muito mais lento (nem
2x).
Nas gramáticas livres de contexto os métodos recursivos com retrocesso apresentam
uma demora em relação ao método sem retrocesso porque estes têm a preocupação de
salvar a posição atual na string de entrada antes de efetuarem uma derivação.
Por fim, as gramáticas de atributos são praticamente equivalentes às versões livres de
contexto mais complexas. Em particular a linguagem Java tem bom desempenho.
Esta tabela comparativa traz uma visão abrangente da performance de diferentes
métodos de codificação de gramáticas, permitindo uma criteriosa escolha da linguagem
principalmente em termos de performance. Por exemplo, se desejamos codificar uma pequena gramática onde a performance não é tão crı́tica podemos fazê-lo em Java. Por outro
lado, a linguagem C(C++) se mostrou mais eficiente na codificação destas gramáticas.
Num projeto de ferramentas para processadores de linguagens, vemos que a codificação
de um método declarativo como o DRCR (com costura) é tão eficiente como uma versão
similar mais procedural, o DRCR (salvando o ponteiro). Esta informação é útil na escolha
de um método: devemos procurar utilizar um método mais declarativo.
A razão pela qual o uso de macros #define no C++ acelera o desempenho no processamento das gramáticas é que as funções x(c) e n() são substituı́das pelas macros
CAPÍTULO 3. TÉCNICAS PARA PROGRAMAÇÃO DE GRAMÁTICAS
55
(s[p]==c) e (++p) respectivamente, assim não realizando nenhuma chamada de função
(evita a criação de um registro de ativação de função para avançar o ponteiro sobre a fita
de entrada), tornando o programa mais eficiente.
Ver abaixo um exemplo de gramática codificada em C++, com contagem de tempo.
1
2
3
4
#include
#include
#include
#include
<iostream.h>
<stdio.h>
<string.h>
<windows.h>
5
6
7
8
9
const max=10000; int i; DWORD start, tempo;
void savetime(); void showtime(); void le_palavra();
char s1[]="aaaaaaaaaaaaaaaaa...bbbbbbbbb...cccccccccccc@"; /* 100 chars */
int p[] = {0};
10
11
12
13
void savetime() {start = ::GetTickCount(); }
void showtime()
{ tempo = ::GetTickCount()-start; cout << "Tempo gasto 1/1000 seg:" << tempo;}
14
15
16
17
18
void
bool
bool
bool
le_palavra() { p[0]=0; }
x(char c) {return(s1[p[0]]==c);}
n() { p[0]++; return(true); }
attr(int v[],int exp) {v[0] = exp; return(true);}
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
bool A(int n1[])
{ return(x(’a’) && n() && A(n1) && attr(n1,n1[0]+1) || true && attr(n1,0));}
bool B(int n1[])
{ return(x(’b’) && n() && B(n1) && attr(n1,n1[0]+1) || true && attr(n1,0));}
bool C(int n1[])
{ return(x(’c’) && n() && C(n1) && attr(n1,n1[0]+1) || true && attr(n1,0)); }
bool S()
{int na[] = {0}; int nb[] = {0}; int nc[] = {0};
return(A(na) && B(nb) && C(nc) && (na[0]==nb[0]) && (nb[0]==nc[0])); }
void main()
{
le_palavra();
savetime();
for (int j=0;j<=max;j++)
{
p[0]=0;
if (S() && x(’@’))
/*cout << "Reconheceu"*/ ;
else if (x(’@’)) cout << "Erro nos valores de n";
else cout << "Erro na posicao: " << p[0];}
showtime();}
CAPÍTULO 3. TÉCNICAS PARA PROGRAMAÇÃO DE GRAMÁTICAS
3.2
56
Programação de gramáticas regulares
Método 3.2.1 (Autômato com GOTO) Aplica-se para os autômatos finitos determinı́sticos e as gramáticas regulares do tipo GLUD.
Para os diversos programas apresentados aqui precisamos de um ”preâmbulo” onde
são definidos os tipos de dados, um procedimento básico de inicialização e outras funções
úteis. No preâmbulo uma string s guarda uma palavra lida para ser testada. No final
da palavra lida acrescentamos um caractere fim que pode ser qualquer caractere desde
não pertença ao alfabeto da gramática sendo programada (por exemplo, um @). Uma
variável p implementa um ponteiro (inteiro) que aponta para o caractere corrente na fita
de entrada.
Duas funções com resultado boolean são codificadas para auxiliar na programação
das gramáticas:
• x(c:char):bool – a função x testa se o caractere c passado como parâmetro é o
caractere corrente na entrada, sendo apontado por p; em caso afirmativo o resultado
é true.
• np:bool – a função np avança o ponteiro p uma posição sobre a fita de entrada;
sempre retorna true (o importante desta função é o seu efeito colateral).
A função np é definida para ser utilizada dentro de uma expressão booleana, que será
avaliadas com curto-circuito.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{- preambulo -}
type bool=boolean;
const fim = ’@’;
var s:string; p:integer;
procedure le_palavra;
begin
p:=1;
writeln(’Digite uma palavra:’);
readln(S);
S:=S+fim;
end;
{--}
function x(c: char):bool; begin x:= S[p]=c; end;
function np
:bool; begin p:= p+1; np:=true; end;
Com as funções x e np, pelo método do Autômato com GOTO, podemos programar
a versão GLUD da gramática da linguagem a*b*, descrita abaixo.
1
2
R --> A B
A --> a A | []
%% vers~
ao original
CAPÍTULO 3. TÉCNICAS PARA PROGRAMAÇÃO DE GRAMÁTICAS
3
57
B --> b B | []
4
5
6
A --> aA | bB | []
B --> bB | []
%% vers~
ao GLUD
No programa Pascal, abaixo, temos a tradução da gramática GLUD onde cada produção corresponde a uma linha do código e vice versa:
• cada não terminal é definido como um label (um estado do autômato);
• acrescentamos também um estado final, para verificar se o reconhecedor chegou
no fim da palavra, emitindo a mensagem RECONHECEU ou ERRO na posição
indicada por p;
• cada produção na forma A--> w B é implementado por um comando
if x(w) and np then goto B;
• cada produção na forma A--> [] é implementado por um comando goto final;
• duas alternativas de uma mesma produção são ligadas pelo comando else
Para um autômato o método é ainda mais direto: cada estado é um label e cada
transição é um comando goto.
No programa abaixo é necessário incluir o ”preâmbulo” que define os tipos de dados,
constantes e as funções x e np. Antes de entrarmos no estado inicial devemos chamar o
procedimento le_palavra.
1
2
3
4
5
6
7
8
9
10
11
12
13
{- incluir o pre^
ambulo -}
label a, b, final;
begin
le_palavra;
goto a;
a:
if x(’a’) and np then goto a
else if x(’b’) and np then goto b
else
goto final;
b:
if x(’b’) and np then goto b
else
goto final;
final: if x(fim) then writeln(’Reconheceu’)
else writeln(’Erro na posiç~
ao: ’,p);
end.
Segue abaixo um teste para uma palavra válida e para uma palavra inválida.
’Digite uma palavra:’
aaaabbb<enter>
Reconheceu
CAPÍTULO 3. TÉCNICAS PARA PROGRAMAÇÃO DE GRAMÁTICAS
58
’Digite uma palavra:’
aaaacbbb<enter>
Erro na posiç~
ao: 5
Método 3.2.2 (Autômato com IF-WHILE) Aplica-se principalmente para expressões
regulares (representando autômatos determinı́sticos). É melhor que as produções da
gramática estejam agrupadas nos quatro tipos de construções de expressões regulares:
seqüência, alternativa; repetição vazia e não vazia. Este método pode também ser
usado em gramáticas regulares, mas neste caso devemos identificar e agrupar as produções
nos três tipos de construções básicas.
Vamos apresentar este método numa linguagem regular definida pela expressão a∗ (b|c)+ d+ .
Esta linguagem tem os quatro tipos de construções das linguagens regulares:
• repetição com um obrigatório, (b|c)+ e d+ ;
• repetição com vazio, a∗ ;
• alternativas (b|c)
• seqüência, entre elas;
Para cada uma destas construções temos um esquema de programação, usando a combinação dos comandos (;) (if) (while), como segue:
• repetições, com pelo menos um, são programadas com a combinação de if-while;
• repetições vazias são programadas com while;
• alternativas são programadas com or ou com if-then-else;
• seqüências são codificadas pela ordem dos comandos ligados por (;) – que é o
comando de seqüência no Pascal;
Quando um elemento obrigatório falha, devemos emitir uma mensagem de erro, como
segue:
if x(d) and np then begin ...PROX DA SEQUENCIA...end
else writeln(’Esperado (d) na posiç~
ao: ’,p);
Segue o código para a expressão regular: {a∗ (b|c)+ d+ }.
1
2
3
4
5
{- incluir o pre^
ambulo -}
begin
le_palavra;
{-A-}
while x(a) and np do begin end;
{- a*
-}
CAPÍTULO 3. TÉCNICAS PARA PROGRAMAÇÃO DE GRAMÁTICAS
6
7
8
9
10
11
12
13
14
15
16
17
18
59
if x(b) and np or x(c) and np
{- b|c (1ro) -}
then begin
{-(b|c)*
-}
while x(b) and np or x(c) and np do begin end;
if x(d) and np
{- d (1ro)
-}
then begin
while x(d) and np do begin end; {- d* -}
if x(fim) then writeln(’Reconheceu’)
else writeln(’Erro na posiç~
ao: ’,p);
end
else writeln(’Esperado (d) na posiç~
ao: ’,p);
end
else writeln(’Esperado (b) ou (c) na posiç~
ao: ’,p);
end.
Este método não funciona para expressões regulares não determinı́sticas, como por
exemplo, (ab|ac). O não determinismo exige retrocesso no processamento, assunto a ser
discutido mais adiante.
Método 3.2.3 (Descendente recursivo sem retrocesso (DRSR)) É um método
eficiente. Aplica-se a gramáticas regulares e livres de contexto, desde que (1) não sejam
recursivas à esquerda e também que (2) estejam fatoradas (da classe LL(1)).
Devemos ter cuidado pois a recursividade à esquerda pode aparecer de forma indireta,
como exemplificado abaixo.
1
2
3
E-->K*K
E-->0|1
K-->E+E
A linguagem regular a*b*, que foi programada com GOTO, pode também ser programada pelo método de análise descendente recursiva. Vamos programar com este método
a versão original (não GLUD), que segue.
1
2
3
R --> A B
A --> a A | []
B --> b B | []
O método descendente recursivo consiste em codificar uma função recursiva para cada
não terminal:
• duas alternativas de uma mesma produção são ligadas por um OR;
• dois elementos no lado direito da produção são ligados por AND;
• um elemento vazio é programado como true;
CAPÍTULO 3. TÉCNICAS PARA PROGRAMAÇÃO DE GRAMÁTICAS
60
• um terminal t é programado com "x(t) and np";
Para facilitar a programação, inicialmente definimos todos os não terminais como
protótipos de funções booleanas, com a diretiva forward do Pascal. A seguir definimos
estas funções na mesma ordem em que são listadas na gramática. Por exemplo, o não
terminal A --> a A | [] gera a expressão A:= X(’a’) and np and A or true.
No programa principal chamamos a função associada ao sı́mbolo inicial da gramática
e testamos se toda a palavra foi consumida (se o próximo sı́mbolo é o fim da fita); neste
caso a palavra foi reconhecida.
1
2
3
{- incluir preambulo -}
function A: bool; forward;
function B: bool; forward;
4
5
6
7
8
9
10
11
12
13
14
function R: bool; forward;
{--}
function R; begin R:= A and B; end;
function A; begin A:= x(’a’) and np and A or true; end;
function B; begin B:= x(’b’) and np and B or true; end;
begin {- principal -}
le_palavra;
if R and x(fim) then writeln(’Reconheceu’)
else writeln(’Erro na posiç~
ao:’, p);
end.
Os testes indicados para a versão programada com GOTO, acima, também se aplicam
a esta versão DRSR da gramática.
Numa linguagem imperativa, um método simples e rápido de codificar um autômato é
programa-lo com GOTO. Apesar dos livros didáticos não recomendarem o uso de GOTO,
eles deixam o programa com a ”cara” da especificação; bem dentro das normas da Engenharia de Software: cada linha do programa é uma transição do autômato e vice-versa.
Assim, se for mudada a especificação será direta a mudança do código.
3.3
Programação de gramáticas livres de contexto
O método DRSR, apresentado acima, se aplica também para gramáticas livres de
contexto. Porém, ele funciona somente para gramáticas já fatoradas. Por exemplo, a
gramática abaixo não é fatorada.
1
2
L--> a L b
L--> a b
Podemos fatorar esta gramática acrescentando uma produção auxiliar L1 como é dada
abaixo.
CAPÍTULO 3. TÉCNICAS PARA PROGRAMAÇÃO DE GRAMÁTICAS
1
2
3
61
L --> a L1
L1 --> L b
L1 --> b
Agora utilizando o método já exposto programamos a gramática fatorada, como segue.
1
2
3
4
5
6
7
8
9
10
11
{- incluir preambulo -}
function L : bool; forward;
function L1: bool; forward;
{--}
function L; begin L := x(’a’) and np and L1; end;
function L1; begin L1 := (L and x(’b’) and np) or (x(’b’) and np) end;
begin {- principal -}
le_palavra;
if R and x(fim) then writeln(’Reconheceu’)
else writeln(’Erro na posiç~
ao:’, p);
end.
Para testa-la utilizamos três palavras: aaabbb é uma palavra válida; aabbbb sobram
bs na fita de entrada – erro na posição 5; e a palavra aaabb faltam bs, erro na posição 6,
que é o caractere de fim da fita.
’Digite uma palavra:’
aaabbb<enter>
Reconheceu
’Digite uma palavra:’
aabbbb<enter>
Erro na posiç~
ao: 5
’Digite uma palavra:’
aaabb<enter>
Erro na posiç~
ao: 6
Podemos também programar uma gramática não fatorada, neste caso usa-se o método
de análise descendente recursivo com retrocesso.
Método 3.3.1 (Descendente recursivo com retrocesso (DRCR)) Aplica-se a
gramáticas regulares e livres de contexto, desde que (1) não sejam recursivas à esquerda.
Usa-se quando uma gramática não está fatorada (possui produções não determinı́sticas).
Existem dois submétodos de implementação: no primeiro, salvamos explicitamente
o ponteiro da fita p e o restauramos nos pontos de alternativas se for necessário; e no
segundo, fazemos uso da um ponteiro local a cada produção.
CAPÍTULO 3. TÉCNICAS PARA PROGRAMAÇÃO DE GRAMÁTICAS
62
Método 3.3.2 (Descendente recursivo com retrocesso (DRCR-salva-p)) É
um submétodo do DRCR – muda apenas na forma de retroceder: salvando o ponteiro p.
Aqui o retrocesso é implementado sobre a fita de entrada. Antes de entrar numa
alternativa de uma produção salva-se o p; e se durante o reconhecimento descobrimos
que a alternativa da produção não devia ser utilizada então retrocedemos o p e tentamos
a próxima alternativa. Segue o código para esta versão.
1
2
3
4
5
6
7
8
9
10
function L: bool;
var salvap: integer;
begin
salvap:= p;
if (X(’a’) and np and X(’b’) and np) then L:= true
else begin
p:= salvap;
if (X(’a’) and np and L and X(’b’) and np) then L:= true
else L:= false;
end;
11
12
13
14
15
16
17
end;
begin {- principal -}
le_palavra;
if L and x(fim) then writeln(’Reconheceu’)
else writeln(’Erro na posiç~
ao:’, p);
end.
Como vemos o código fica com uma cara procedimental, pois somos obrigados a usar
os comandos (if), (:=) e (begin end) para implementar o retrocesso.
Os mesmos testes feitos para o método DRSR se aplicam nesta versão.
Método 3.3.3 (Descendente recursivo com retrocesso(DRCR-com costura))
É um submétodo do DRCR - varia apenas na forma de retroceder: implementa o retrocesso numa forma declarativa.
Este método é mais geral e limpo para implementar analisadores descendentes recursivos com retrocesso; ele é inspirado no mecanismo DCG do Prolog. Vamos exemplificar
na gramática L não fatorada.
1
2
3
% regras n~
ao fatoradas
L--> a L b
L--> a b
4
5
6
L(i,o) --> x(a,i) L(i+1,i1) x(b,i1+1) {o:=i1+1}
L(i,o) --> x(a,i) x(b,i+1) {o:=i+2}
%% vers~
ao (a)
CAPÍTULO 3. TÉCNICAS PARA PROGRAMAÇÃO DE GRAMÁTICAS
63
7
8
9
L(i,o) --> x1(a,i,i1) L(i1,i2) x1(b,i2,o)
L(i,o) --> x1(a,i,i1) x1(b,i1,o)
%% vers~
ao (b)
Neste método é necessário programar uma gramática de atributos GA para simular o
p. Simular o p consiste em contar os tokens consumidos da fita de entrada. Usa-se dois
atributos, um i (input) e um o (output). Após consumir um token a incrementamos o
i; o L retornará um i1 com um valor do p na saı́da. Por exemplo, na palavra aaabbb
= aLb. o L retorna um i1=6 apontando para o último b; após consumido este b, o i1 é
incrementado e devolve-se o=7, apontando para o token fim.
Para esta GA funcionar temos uma nova função x(Token,p), onde passamos o token
e a posição do p. Para não modificarmos a função x preferimos a versão (b) da GA
onde temos uma nova função x1(Token, i, o) que ao mesmo tempo testa pelo Token e
incrementa o p.
•
c:char – idem versão inicial – testa o token;
•
i:int – indicando o valor de p na entrada (atributo herdado); e;
• var o:int – indicando o valor de p na saı́da (atributo sintetizado).
Na versão (b), em cada alternativa de produção o valor do i de entrada é passada
adiante, no corpo da regra, para o próximo elemento depois de incrementado, até no
último elemento da alternativa, que corresponde ao o, que é retornado da função. Por
isso também é chamado de método da costura.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{- include preambulo -}
{- vers~
ao (b)
-}
{- ---------------------------}
{- gramática L --> a L b
-}
{L --> a b
-}
{- ---------------------------}
var pe:int; {posiç~
ao do erro}
function x(c:char i:int; var o:int): bool; begin x:=S[i]=c; o:=i+1; pe:=o;end;
function L(i:int; var o:int): bool; forward;
{--}
function L(i:int; var o:int):bool; var i1,i2:int; begin
L := x(’a’,i,i1) and L(i1,i2) and x(’b’,i2,o) or
x(’a’,i,i1) and
x(’b’,i1,o); end;
var o,o1:int;
begin {- principal -}
le_palavra;
if L(1,o) and x(fim,o,o1) then writeln(’Reconheceu’)
else writeln(’Erro na posiç~
ao:’, pe);
end.
CAPÍTULO 3. TÉCNICAS PARA PROGRAMAÇÃO DE GRAMÁTICAS
64
Neste método não temos um p global, ele é implementado localmente em cada função
(ou produção). Por isso, caso uma produção falha o p não precisa ser retrocedido. A
função np também não é necessária pois é codificada dentro da função x1.
Aqui temos uma variável global pe que guarda a posição do último token manipulado,
como a posição de erro. Uma melhoria no diagnóstico de erros para o método DRCR é
salvar em pe a posição mais avançada sobre a fita de
entrada: i.e., o pe não deveria retroceder. Pode ser programado testando-se se o valor
a ser atribuı́do ao pe é maior que o valor corrente. Se sim modifica-lo, senão não fazer
nada. Ver exercı́cio abaixo.
Este método de implementação de analisadores descendentes recursivos com retrocesso
é geral, porém ele deve ser utilizado somente se uma gramática não é fatorada, pois usa
mais recursos computacionais: dois parâmetros em cada função mais as variáveis locais
(em L: i1, i2) para implementar o p.
Exercı́cio 3.3.1 Programe a gramática livre de contexto abaixo, com o método DRCR;
programe as produções na mesma ordem que acontecem.
1
2
3
4
5
6
7
W
A
A
A
A
B
B
-->
-->
-->
-->
-->
-->
-->
A B
a a a A
a a A
a A
a
b B
[]
Teste o programa para as palavras; abb – o sistema retrocede nas alternativas 2 e 3;
aabb – o sistema retrocede na alternativa 2; aaabb não é necessário o retrocesso; – aacb
– erro na posição 3; aaac – erro na posição 4. Veja a dica acima para dar este diagnóstico
de erro.
Uma GA tem o poder computacional de processar gramáticas sensı́veis ao contexto.
Aqui nós estamos usando uma GA para implementar um mecanismo de retrocesso para
uma gramática livre de contexto. Sempre podemos usar um formalismo de uma classe
superior de linguagens para processar uma classe inferior. Na próxima seção mostraremos
como codificar gramáticas sensı́veis ao contexto como GAs.
Exercı́cio 3.3.2 Programe os comentários da linguagem C++, de forma que possam vir
aninhados. Use o método DRSR. É necessária uma gramática livre de contexto. Eles
podem ser de dois tipos: (1) dupla // até o fim de linha e (2) múltiplas linhas com abre e
fecha /* */ como exemplificado abaixo. Um abre comentário sem um fecha correspondente
é erro.
1
2
3
void f()
{ cout << 10;
/* este /* eh um */ comentario */
// funcao vazia
CAPÍTULO 3. TÉCNICAS PARA PROGRAMAÇÃO DE GRAMÁTICAS
/* comentario // de duas linhas
esta eh /* a segunda */
*/
// este tambem eh
4
5
6
};
3.4
65
Programação de Gramáticas de Atributos (GAs)
Para concluir a visão geral de codificação de gramáticas em linguagens imperativas
vamos mostrar a programação de uma GA.
Método 3.4.1 (Gramáticas de atributos)) GAs são baseadas em GLCs, portanto
usa-se os mesmos métodos para codificação. Os atributos sintetizados são programados
com parâmetros de retorno; e, os herdados como parâmetros só de entrada.
A implementação da análise DRCR foi exemplificada para a gramática L, na seção
anterior, como uma solução baseada em GA: foram usados dois atributos (i-input, ooutput) um herdado e outro sintetizado.
Segue um exemplo simples com um atributo sintetizado (n). Considere a GA para a
linguagem an bn cn .
1
2
3
4
5
6
7
S
A(1+n)
A( 0 )
B(1+n)
B( 0 )
C(1+n)
C( 0 )
-->
-->
-->
-->
-->
-->
-->
A(n) B(n) C(n).
a A(n).
[]
b B(n). %% n: atributo sintetizado
[]
c C(n).
[]
As produções desta gramática livre de contexto são fatoradas e sem recursividade à
esquerda. Portanto pode ser programada pelo método DRSR - descendente recursivo sem
retrocesso.
Para fazer a contagem do número de as, bs e cs definimos um predicado attr(V,EXP)
que simula uma atribuição V:=EXP do Pascal – o valor da expressão é calculado e atribuı́do
a variável V. Para garantir que teremos o mesmo número de as, bs e cs, usamos os
predicados na=nb and nb=nc no final da produção s.
1
2
3
4
5
6
7
8
{- incluir pre^
ambulo-}
function attr(var v:int; exp:int):bool;begin v:=exp; attr:=true; end;
{--}
function S
:bool; forward;
function A(var n:int):bool; forward;
function B(var n:int):bool; forward;
function C(var n:int):bool; forward;
{--}
CAPÍTULO 3. TÉCNICAS PARA PROGRAMAÇÃO DE GRAMÁTICAS
9
10
11
12
13
14
66
function S; var na, nb, nc:int;begin
S:= A(na) and B(nb) and C(nc) and na=nb and nb=nc; end;
function A(var n:int):bool; begin
A:= x(’a’) and np and A(n1) and attr(n,n1+1) or
true and attr(n,0); end;
function B(var n:int):bool; begin
15
16
17
18
19
20
21
22
23
24
B:= x(’b’) and np and B(n1) and attr(n,n1+1) or
true and attr(n,0); end;
function C(var n:int):bool; begin
C:= x(’b’) and np and C(n1) and attr(n,n1+1) or
true and attr(n,0); end;
begin {- principal -}
le_palavra;
if S and x(fim) then writeln(’Reconheceu’)
else if x(fim)
writeln(’Erro nos valores de n’
25
26
27
else
end.
3.4.1
writeln(’Erro na posiç~
ao:’, p);
Método da costura com atributos
Abaixo mostramos que podemos combinar o método da costura com o método que
codifica gramáticas de atributos; segue um exemplo.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{- include preambulo -}
{- vers~
ao (b)
-}
{- -----------------------------------}
{- gramática L(N+1) --> a L(N) b
-}
{L( 1) --> a b
-}
{- ---------------------------=-------}
var pe:int; {posiç~
ao do erro}
function x(c:char i:int; var o:int): bool; begin x:=S[i]=c; o:=i+1; pe:=o;end;
function attr(var v:int; exp:int):bool;begin v:=exp; attr:=true; end;
function L(var No:int i:int; var o:int): bool; forward;
{--}
function L(var No:int; i:int; var o:int):bool; var i1,i2, N:int; begin
L := x(’a’,i,i1) and L(N, i1,i2) and x(’b’,i2,o) and attr(No,N+1) or
x(’a’,i,i1) and
x(’b’,i1,o) and attr(No, 1); end;
var o,o1, N:int;
begin {- principal -}
le_palavra;
if L(N,1,o) and x(fim,o,o1) then writeln(’Reconheceu, nı́veis =’,N)
CAPÍTULO 3. TÉCNICAS PARA PROGRAMAÇÃO DE GRAMÁTICAS
else writeln(’Erro na posiç~
ao:’, pe);
19
20
67
end.
3.4.2
Exercı́cios de Revisão
Segue uma relação de exercı́cios simples, com base em versões de GA já discutidas.
Exercı́cio 3.4.1 Programe a GA dada abaixo, com atributos herdados e sintetizados,
numa linguagem imperativa.
1
2
3
4
5
%% L-GA em DCG
b0(
M )--> {AC:=0}, b(AC,M).
b(AC, M )--> [b], {AC1:=AC+1}, b(AC1,M).
b(ACi,ACo)--> [], {ACo:=ACi}.
%% AC é um ACumulador
Exercı́cio 3.4.2 Programe a GA dada abaixo, na notação de Knuth, com atributos herdados e sintetizados, numa linguagem imperativa.
n
bs1
bs
b
b
-->
-->
-->
-->
-->
bs
b bs2
[]
0
1
{bs.N
{bs2.N
{bs.R
{b.B
{b.B
:=
:=
:=
:=
:=
0; n.R := bs.R}
2*bs1.N+b.B; bs1.R := bs2.R}
bs.N}
0}
1}
Exercı́cio 3.4.3 Programe a DCG abaixo como uma GA numa linguagem imperativa.
?-move(F,D,[esq,dir,esq,frente,frente,dir,dir,pare],[]).
F= 0+0+0+1+1+0+0+0,
D=-1+1-1+0+0+1+1+0 Yes
1
2
3
4
5
6
7
move(0,0) --> [pare].
move(D,F) --> passo(D,F).
move(D+D1,F+F1)--> passo(D,F), move(D1,F1).
passo( 1, 0) --> [dir].
passo(-1, 0) --> [esq].
passo( 0, 1) --> [frente].
passo( 0,-1) --> [traz]
Capı́tulo 4
Programação de Gramáticas em
Prolog
Este capı́tulo apresenta o uso prático das técnicas de análise léxica, sintática e semântica,
usando os recursos de programação do Prolog.
Este capı́tulo apresenta duas principais seções: (1) análise sintática e semântica e (2)
análise léxica. Agrupamos o conteúdo sintático e semântico na mesma seção porque é
difı́cil separamos a sintaxe da semântica em aplicações práticas.
Na análise léxica são explorada duas abordagens. A primeira é o uso direto de DCG
que tem o poder de codificar regras com retrocesso (regras não fatoradas). A segunda faz
uso de predicados recursivos. Ela é ilustrada num exemplo que trabalha com um arquivo
de entrada (como uma fita de caracteres) fazendo uso das operações de entrada e saı́da
do Prolog. Nesta segunda abordagem a gramática deve estar fatorada.
68
CAPÍTULO 4. PROGRAMAÇÃO DE GRAMÁTICAS EM PROLOG
4.1
69
Análise sintática e semântica
Método 4.1.1 (DCG para análise sintática e semântica) DCGs em Prolog implementam o método de análise sintática descendente recursiva com retrocesso (gramáticas
LL(k)). Para eliminar o retrocesso basta fatorar a gramática. Quanto estendida por
parâmetros, uma DCG implementa uma Gramática de Atributos, que é uma formalismo
para especificar semântica de linguagens.
Nesta seção temos dois estudos de casos para problemas de análise sintática e semântica:
o primeiro é para uma mini linguagem chamada LET para expressões aritméticas; o segundo é para um tradutor que traduz alguns tipos de comandos SQL para álgebra relacional. Estes exemplos ilustrar o poder expressivo de DCG para codificar processadores
de linguagens de programação.
4.1.1
Calcular expressões aritméticas com variáveis
A linguagem LET, descrita aqui, é uma mini linguagem interessante para ser estudada,
pois exige uma tabela de sı́mbolos com contexto para armazenar as variáveis parciais
usadas numa expressão aritmética. Ela permite calcular expressões LET aninhadas como
as que seguem:
let a=4+5, b=8+2
in a + b
VALOR=(4+5)+(8+2) = 19
let c= (let a=4+5, b=8+2
%% aqui o par^
entese é opcional
in a + b),
%% porém facilita a leitura
d=8+2
in (c+d)*c+d
VALOR=(4+5+ (8+2)+ (8+2))* (4+5+ (8+2))+ (8+2)= 561
Abaixo temos uma gramática para estas expressões. Primeiro codificamos dois predicados para implementar uma tabela de sı́mbolos, como uma lista de pares par(VAR,VAL):
• lookUp/2 — retorna o valor para uma variável;
• insert/2 — insere um par(VAR,VAL) na tabela de sı́mbolos.
Como temos expressões aritméticas simples, do tipo (c+d)*c+d, a idéia é reusar uma
versão simplificada da gramática de expressões já discutida. Não trabalharemos com as
operações de soma e divisão pelo problema de associatividade já discutido. Agora todas
as produções recebem um atributo herdado, que é a tabela de sı́mbolos. Ela é necessária
pois agora um fator pode ser uma variável: neste caso o seu valor está registrado na
tabela de sı́mbolos (para um expressão bem formada).
Na gramática LET são usadas três novas produções let, decVar e decVars A produção let define uma expressão composta de declaração e o corpo da expressão onde as
CAPÍTULO 4. PROGRAMAÇÃO DE GRAMÁTICAS EM PROLOG
70
declarações serão usadas. A produção decVar declara uma variável associada a uma expressão - no final a variável e a expressão são incluı́das na tabela de sı́mbolos. A produção
decVars declara um ou mais pares Var=Exp separados por vı́rgula.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
lookUp(X,T):-member(X,T).
insert(X,Ti/To):-To=[X|Ti], write((tab:To)),nl.
isLetra(X):-member(X,[a,b,c,d,e,f,g,h,i,x,y,z]).
%%
let(Ti,V) --> [let], decVars(Ti/T1), [in], expr(T1,V).
decVars(Ti/To) --> decVar(Ti/T1), [’,’], decVars(T1/To).
decVars(Ti/To) --> decVar(Ti/To).
decVar(Ti/To) --> [L],{isLetra(L)}, [=], expr(Ti,E),
{insert(par(L,E),Ti/To)}.
%%
expr(TAB,E)--> let(TAB,E).
expr(TAB,E)--> termo(TAB,T),[+],expr(TAB,Eo),{E = (T+Eo)}.
expr(TAB,E)--> termo(TAB,E).
termo(TAB,T)--> fator(TAB,F),[*],termo(TAB,To),{T = (F*To)}.
termo(TAB,F)--> fator(TAB,F).
fator(TAB,X)--> [X],{integer(X)}.
fator(TAB,E)--> [’(’],expr(TAB,E), [’)’].
fator(TAB,V)--> [X],{member(X,[a,b,c,d,e,f,g,h,i,x,y,z])},
{lookUp(par(X,V),TAB), write((look:X:V)),nl}. %% vars
Com é difı́cil digitar corretamente uma destas expressões para teste, codificamos dois
testes como predicados, como segue.
1
2
3
4
5
6
7
%% ?- teste(1,LET),let([],V,LET,RESTO), VAL is V.
teste(1, [let, a,=,4,+,5,’,’,b,=,8,+,2,
in, a, +, b]).
teste(2, [let, c,=,let, a,=,4,+,5,’,’,b,=,8,+,2,
in, a, +, b,’,’,
d,=,8,+,2,
in, ’(’, c, +, d,’)’, *, c, +, d ]).
Abaixo segue a execução dos testes. Incluı́mos dois write(s) para depurar o programa. Aqui vemos que este programa trabalha com retrocesso: em alguns casos ele
inclui na tabela de sı́mbolos resultados que ainda não são definitivos; ao mesmo tempo
ele acessa a tabela varias vezes desnecessariamente. Estes problemas são resolvidos com
a fatoração do programa, deixado como exercı́cio.
?- teste(1,LET),let([],V,LET,RESTO),VX is V.
CAPÍTULO 4. PROGRAMAÇÃO DE GRAMÁTICAS EM PROLOG
tab:[par(a,
tab:[par(b,
tab:[par(b,
tab:[par(b,
look:a:4+5
look:a:4+5
look:b:8+2
look:b:8+2
look:b:8+2
look:b:8+2
71
4+5)]
8+2), par(a, 4+5)]
8), par(a, 4+5)]
8+2), par(a, 4+5)]
LET = [let, a, =, 4, +, 5, (’,’), b, =|...]
V = 4+5+ (8+2)
RESTO = []
VX = 19
?- teste(2,LET),let([],V,LET,RESTO), VX is V.
tab:[par(d, 8+2), par(c, 4+5+ (8+2))]
look:c:4+5+ (8+2)
look:d:8+2
LET = [let, c, =, let, a, =, 4, +, 5|...]
V = (4+5+ (8+2)+ (8+2))* (4+5+ (8+2))+ (8+2)
RESTO = []
VX = 561
Exercı́cio 4.1.1 Fatore o programa da gramática. Lembre que ao fatorar deve-se ajustar
as equações semânticas. Inclua corte onde for necessário para que ele trabalhe de forma
determinı́stica.
Exercı́cio 4.1.2 Implemente uma técnica de diagnóstico para erro sintático. Por exemplo, na gramática fatorada, escrevendo até onde o programa reconheceu.
Exercı́cio 4.1.3 Implemente um diagnóstico para erros semânticos. As variáveis declaradas em vars numa expressão let vars in expr só podem ser usadas num contexto
mais interno in expr. Seguem abaixo dicas para diagnóstico de erros semânticos.
let a=b+5, b=8-2 /** em a=b+5 a variável b ainda n~
ao foi declarada **/
in let c=a+b, d=a+a+3
in (c+d)*(c+d)/2
let a=b1+5, b=let k=2+3 in k+k
in (b+c+k)
/** em b+c+k a variável k já n~
ao existe **/
72
CAPÍTULO 4. PROGRAMAÇÃO DE GRAMÁTICAS EM PROLOG
/** ela é local ao outro let
let a=5, b=8-2
in let a=a+1
in a+b
**/
/** esta express~
ao é válida e o aqui o a=6 **/
/** vale a declaraç~
ao mais interna, isto já funciona **/
Modifique o predicado do lookUp para dar o diagnóstico dizendo quando ele não
encontra a variável na tabela de sı́mbolos.
Exercı́cio 4.1.4 Estenda esta gramática para trabalhar com qualquer tipo de expressões
aritméticas, e avaliar corretamente.
4.1.2
Traduzir SQL para álgebra relacional
Outro exemplo interessante de análise sintática e semântica é apresentado aqui. Ele
é da área de BD: é a tradução de um comando SQL para um seqüência de comandos
de álgebra relacional. O objetivo aqui não é fazer um interpretador de comandos SQL
mas sim ilustrar como os comandos SQL podem ser traduzidos para álgebra relacional.
Fazer um interpretador de SQL é um trabalho bem mais complexo devido a necessidade
de tratar temas tais como otimização de consultas.
Supomos o seguinte esquema de BD e as primitivas da álgebra relacional dadas abaixo:
Compra ( cNoItem {numero do item},
cNoForn {numero do fornecedor},
cQuantidade )
Fornecedor ( fNoForn {fornecedor numero},
fNome
{fornecedor nome } )
PROJECT (lista-de-campos, tabela)
SELECT (condicao,
tabela)
JOIN
(condicao,
tabela1, tabela2)
Uma consulta de SQL pode ser traduzida para Álgebra Relacional como exemplificado
abaixo:
SELECT cNoItem, cQuantidade
FROM
Compra
WHERE cNoForn IN (SELECT fNoForn
FROM
Fornecedor
WHERE fNome = ’joaozinho’)
PROJECT( [cNoItem, cQuantidade],
JOIN( [cNoForn=fNoForn],
Compra,
PROJECT( [fNoForn],
SELECT( [fName = ’joaozinho’],
Fornecedor))))
CAPÍTULO 4. PROGRAMAÇÃO DE GRAMÁTICAS EM PROLOG
73
São usadas quatro produções principais: sql – define uma construção de projeção;
relation – define uma construção de seleção; condition – complementa a seleção com
um caso para a operação de join e attrList – define uma lista de atributos para uma
relação.
Os sı́mbolos terminais são os operadores relacionais, os identificadores para nomes de
relações e de atributos; e, as constantes numéricas ou alfanuméricas.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql(REL,ATTR)-->[select], attrList(LIST), [from], relation(REL2),
{REL =[’\n PROJECT ( [’, LIST, ’]’, REL2, ’)’],
ATTR=LIST }.
relation(REL) --> id(ID), [where], condition(OP, COND, NESTED),
{OP=select, REL= [’\n SELECT ( [’, ’[’, COND, ’,’ , ID, ’]’| NESTED],!
;OP=join,
REL= [’\n JOIN ( [’, ’[’, COND, ’,’ , ID, ’]’| NESTED],!}.
condition(select, COND, []) --> id(ID), relOper(OPER), const(CONST),
{COND
= [’(’, ID, OPER, CONST, ’)’ ]}.
condition(join, COND, NESTED) --> id(ID), [in],[’(’], sql(REL,ATTR), [’)’],
{COND
= [’{[’,ID, ’=’, ATTR, ’]’ ],
NESTED = [’,’, REL, ’}’]}.
attrList(LIST) --> id(ID), [’,’], attrList(LIST1),
{LIST = [ID,’,’|LIST1]}.
attrList(ID)--> id(ID).
15
16
17
18
relOper(X) --> [X],{relOper(X)}.
id(X) --> [X],{field(X) ; relation(X)}.
const(X) --> [X],{const(X)}.
19
20
21
\* base de fatos *\
relOper(’=’). relOper(’<’).
22
23
24
field(cNoItem). field(cQuantidade). field(cNoForn).
field(fNoForn). field(fNome).
25
26
relation(’Compra’). relation(’Fornecedor’).
27
28
const(joao). const(10).
Segue abaixo um teste, que gera código para o exemplo dado acima.
1
2
3
4
5
p:-sql(REL,ATTR,
[select, cNoItem, ’,’, cQuantidade,
from , ’Compra’,
where , cNoForn, in, ’(’, select, fNoForn,
from, ’Fornecedor’,
CAPÍTULO 4. PROGRAMAÇÃO DE GRAMÁTICAS EM PROLOG
where, fNome, ’=’ ,joao, ’)’ ],
6
7
8
9
74
RESTO), wL(REL),nl.
wL([X|L]):-!,wL(X),wL(L). wL([]):-!.
wL(X):-write(X).
?-p.
PROJECT ( [cNoItem,cQuantidade]
JOIN ( [[{[cNoForn=fNoForn],Compra],
PROJECT ( [fNoForn]
SELECT ( [[(fNome=joao),Fornecedor])})
[]
Yes
Esta gramática pode ser um ponto de partida para se fazer algum trabalho de uma
disciplina avançada de BD, que estuda a tradução de SQL para álgebra relacional.
4.2
Análise léxica e Autômatos
Em Prolog existem dois métodos de programação de gramáticas regulares, um usando
DCG e o outro usando cláusulas. Este segundo método será exemplificado trabalhando
com arquivos. Iniciaremos com o estudo de gramáticas na notação DCG para um léxico
para expressões aritméticas.
4.2.1
DCGs para análise léxica
Podemos trabalhar com analisadores léxicos usando apenas programação por cláusulas,
especialmente se a gramática regular estiver fatorada. Porém para certas aplicações pode
ser útil trabalharmos com DCG que permite fazer uso de retrocesso.
Método 4.2.1 (DCG para análise léxica) DCGs em Prolog implementam o método
de análise sintática descendente recursiva com retrocesso (gramáticas LL(k)). Para eliminar o retrocesso basta fatorar a gramática.
Tokens para expressões aritméticas
Para processadores de linguagens uma tarefa comum é traduzir uma linha de texto
numa lista de tokens. Por exemplo, abaixo temos um predicado tokens que retorna uma
lista de tokens para um string de uma expressão aritmética:
?- tokens(L,"11 +3*(23)",[]).
L = [int(11), operador(+), int(3),
operador(*), delimitador(’(’), int(23),
delimitador(’)’ ]
CAPÍTULO 4. PROGRAMAÇÃO DE GRAMÁTICAS EM PROLOG
75
Queremos programar este predicado tokens, que recebe uma lista de caracteres e
devolve uma lista de tokens. Aqui temos dois tipos de tokens e um separador:
• inteiros, seqüências de dı́gitos (e.g., 11, 3 e 23);
• sı́mbolos gráficos, são os operadores +, * e os parênteses;
• separadores, são os caracteres não visı́veis; no exemplo, são os espaços em branco,
mas poderia ser também caracteres de tabulação e quebra de linha.
Diferenciamos o termo token de lexema: um token é uma classe para os lexemas; os
lexemas são os elementos de uma classe de tokens. Por exemplo, acima temos os lexemas
(11, 3, e 23), todos do token tipo inteiro.
Uma gramática para tokens aritméticos deve considerar dois tipos de tokens: inteiros
e sı́mbolos e um tipo de separadores (brancos). Um inteiro é uma seqüências de dı́gitos –
(int). Os sı́mbolos gráficos (operadores e delimitadores) são representados por caracteres
únicos entre eles: "+-*/()". Um token ((tok)) é um inteiro ou um sı́mbolo. Uma fita
de tokens é uma seqüência de tokens, que podem estar separados por uma seqüência
de brancos((br)).: os tokens são escritos na saı́da e os brancos são desprezados. Ver a
gramática abaixo.
Figura 4.1: Autômato finito para tokens de expressões aritméticas.
A gramática resultante possui 7 não terminais, com 13 produções. Na Figura 4.1
temos um autômato equivalente. Note que na passagem da gramática para o autômato
alguns não terminais não são representados como estados: por exemplo, temos 7 não
terminais e apenas 4 estados – não temos os estados int, br e tok. Estes estados podem
ser criados com uma transição vazia partindo do estado toks – portanto eles também
podem ser eliminados, eliminando-se três transições vazias.
CAPÍTULO 4. PROGRAMAÇÃO DE GRAMÁTICAS EM PROLOG
1
2
3
4
5
6
7
8
9
10
11
12
13
14
76
int --> dig, rint.
rint --> dig, rint.
rint --> [].
br --> branco, rbr.
rbr --> branco, rbr.
rbr --> [].
%%
tokens --> tok, rtokens.
tokens --> br, rtokens.
rtokens --> tok, rtokens.
rtokens --> br, rtokens.
rtokens --> [].
tok
-->
int.
tok
--> simbolo.
Esta gramática na notação DCG do Prolog, é executável; porém ela deve ser completada pela descrição das produções dig, simbolo e branco. Abaixo temos a definição
destas produções.
1
2
3
branco
--> [C],{C<33}. %% [32]; [8]; [10]; [13].
dig( C ) --> [C],{C>47, C<58}.
simbolo([D]) --> [D],{member(D, "*+/-()")}.
Na definição destas produções, assumimos que vamos trabalhar com uma cadeia de
códigos Ascii dos caracteres. A maneira mais econômica de escrever cadeias de códigos
Ascii é usando aspas como segue:
?- ”0123 89”=X.
X= [48, 49, 50, 51, 32, 56, 57]
Assim, "0"=[48] e "9"=[57], o que significa que o valor ASCII do zero é 48. O
Prolog ISO tem outras notações para representar os valores Ascii, por exemplo, 0’0=48
e 0’9=57. Todos os dı́gitos estão entre estes dois valores, o que resultou na produção:
dig(C)-->[C],{C>47, C<58}
Note que as equações semânticas em DCG são codificadas entre chaves { }. Assim a
produção dig é válida somente se o valor de C estiver dentro do intervalo especificado.
Os separadores (ou brancos) foram definidos como os caracteres que inclui os códigos
menores que 32, o valor Ascii de um espaço em branco. Este conjunto inclui, entre
outros, o caracter de tabulação (8) e os caracteres de fim de linha (13 e 10). Estes
separadores são reconhecidos na produção branco pela restrição {C<33}.
Os sı́mbolos gráficos (operadores e delimitadores) são definidos por uma lista "+-*/()"
que implementa um conjunto usando-se o predicado member.
Abaixo temos uma versão em DCG da gramática para expressões aritméticas. Foram
acrescentados parâmetros para se acumular uma seqüência de dı́gitos para se gerar um
CAPÍTULO 4. PROGRAMAÇÃO DE GRAMÁTICAS EM PROLOG
77
token inteiro; de forma similar guarda-se uma seqüência de tokens dentro de uma lista,
para ser exibida no final do processamento.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int([C|W]) --> dig(C),!, rint(W).
rint([C|W]) --> dig(C),!, rint(W).
rint(
[]) --> [].
br --> branco,!, rbr.
rbr --> branco,!, rbr.
rbr --> [].
%%
tokens([H|T]) --> tok(H),!,rtokens(T).
tokens(
T ) --> br,
!,rtokens(T).
rtokens([H|T]) --> tok(H),!,rtokens(T).
rtokens(
T ) --> br,
!,rtokens(T).
rtokens(
[]) --> [].
tok(int(T)) -->
int(L),!,{name(T,L)}.
tok(simb(T)) --> simbolo(L),!,{name(T,L)}.
%%
branco
--> [C],{C<33}. %% [32]; [8]; [10]; [13].
dig( C ) --> [C],{C>47, C<58}.
simbolo([D]) --> [D],{member(D, "*+/-()")}.
Na prática sempre que implementamos uma gramática regular são necessárias algumas
ações semânticas para gerar os tokens. Ações semânticas são naturalmente integradas nas
regras DCG do Prolog.
Na produção tok o predicado name é usado para transformar uma lista de códigos
Ascii num sı́mbolo. Por exemplo, ?-name([48,49],X). X=01. O operador de corte foi
introduzido em todas as produções tornando o programa deterministico.
Testando o programa
Para testar uma gramática devemos começar com com produções isoladas, por exemplo, int, br, simbolo. Depois testa-se o conjunto como um todo.
?- int(V,"12",D).
V = [49, 50] D = [] Yes
?- br(" ",G).
G = [] Yes
?- simbolo(D,"((",H).
D = [40] H = [40] Yes
Finalmente podemos testar o gerador de tokens com a uma expressão aritmética.
CAPÍTULO 4. PROGRAMAÇÃO DE GRAMÁTICAS EM PROLOG
78
?- tokens(L,"11 +3*(23)",[]).
L = [int(11), simb(+), int(3), simb(*), simb(’(’), int(23), simb(’)’)] Yes
?- tokens(L,"11 +3*(23+32*(3/45)-1)",[]).
L=[int(11),simb(+),int(3),simb(*),simb(’(’),int(23),...] Yes
L = [11, +, 3, *, ’(’, 23, ’)’] Yes
Exercı́cio 4.2.1 Abaixo temos uma saı́da para léxico em que separamos os operadores
(*,+) dos delimitadores (’(’, ’)’). Modifique o programa DCG para gerar esta saı́da.
?- tokens(L,"11 +3*(23)",[]).
L = [int(11), operador(+), int(3),
operador(*), delimitador(’(’), int(23),
delimitador(’)’ ]
Programando não determinismos
O mecanismo DCG do Prolog permite a codificação de regras não determinı́sticas
que olham vários sı́mbolos a frente. Caso uma das alternativas do predicado falha ele
automaticamente retrocede e tenta a próxima alternativa. Por exemplo, podemos codificar quatro regras diferentes para os tokens: ==!, ==:, ==, =; como ilustrado no exercı́cio
abaixo.
Exercı́cio 4.2.2 Codifique em Prolog regras não terministicas para reconhecer os tokens:
==!, ==:, ==, =.
Solução:
1
2
3
4
5
6
igual(’==!’) --> [0’=],[0’=],[0’!],!.
igual(’==:’) --> [0’=],[0’=],[0’:],!.
igual(’==’ ) --> [0’=],[0’=],!.
igual(’=’ ) --> [0’=],!.
%% ?- igual(SIMB, "==: ",[]).
%%
O operador de corte no final das regras visa tornar o programa deterministico. Segue
um teste.
1
2
?- igual(SIMB, "==: ",[]).
SIMB = ’==:’
Exercı́cio 4.2.3 Apesar de podermos trabalhar com regras não fatoradas, aconselha-se
fatorar as produções antes de codifica-las, evitando o retrocesso desnecessário. Defina
regras fatoradas pare reconhecer os quatro sı́mbolos da questão anterior (==!, ==:, ==,
=); codifique-as e teste-as.
Exercı́cio 4.2.4 Desenhe um autômato para as duas versões das regras, fatoradas e não
fatoradas.
CAPÍTULO 4. PROGRAMAÇÃO DE GRAMÁTICAS EM PROLOG
4.2.2
79
Autômatos trabalhando com arquivos
O processamento ao nı́vel léxico consiste em transformar seqüências de caracteres em
tokens ou
palavras. Vimos como usar o formalismo gramatical DCG para codificar gramáticas
regulares. Uma alternativa para implementação é usar cláusulas lógicas e primitivas de
entrada e saı́da.
Autômatos codificados com regras recursivas.
Para implementarmos autômatos não precisamos de regras recursivas, basta usar construtores de iteração tipo o comando while. No Prolog uma iteração é programada com
predicados recursivos. Nesta seção estudaremos a codificação de autômatos finitos determinı́sticos, com predicados recursivas.
Método 4.2.2 (Gramáticas LL(1) como regras recursivas) Dada uma gramática
LL(1) codifica-se cada produção como uma regra recursiva. Para ser LL(1) a gramática
deve estar fatorada. Este método é particularmente válido para Gramáticas regulares do
tipo Linear Unitárias com recursividade à Direita (GLUD). Gramáticas LL(1) (que inclui
a classe GLUD) não exigem retrocesso.
Dada uma grámatica LL(1) fazemos o seguinte codificação:
• cada terminal, no corpo de uma produção, é codificado por um predicado que testa
se ele esta presente na fita de entrada e caso positivo avança o ponteiro da fita;
• cada não terminal é codificado como um predicado;
• cada corpo de produção é codificado imitando o corpo da produção pelas duas
regras já enunciadas;
Por exemplo, seja a especificação abaixo para gerar tokens de dı́gitos como números
inteiros.
1
2
3
4
5
%% LL(1) ou GLUD
digitos --> digito, rdigitos.
rdigitos --> digito,rdigitos.
rdigitos --> [].
digito --> 1|2| ...
6
7
8
9
10
11
%% em DCG, com aç~
oes sem^
anticas para gerar o token
digitos(D)--> digito(L),rdigitos(Ls),{atom_codes(D,[L|Ls])},!.
rdigitos([L|Ls]) --> digito(L),rdigitos(Ls),!.
rdigitos( [] ) --> [],!.
digito(L) --> [L],{isDigito(L)}.
CAPÍTULO 4. PROGRAMAÇÃO DE GRAMÁTICAS EM PROLOG
80
Assumindo que vamos trabalhar com arquivos do Prolog, ele já possui as primitivas
que fazer o lookahead de um caracter sem avançar o ponteiro da fita sobre o arquivo:
• peek_code — retorna o caracter corrente da fita de entrada sem avançar o ponteiro;
• get_code — avança o ponteiro da fita de entrada (consome o caracter).
Usando estas duas primitivas segue a codificação da gramática com e sem ações
semânticas. Note que as ações semânticas não estão entre chaves pois aqui não estamos
trabalhando com DCG. Mesmo na versão sem ações semânticas precisamos pegar o caracter da fita de entrada e testa-lo para ver se é um dı́gito. Na desigualdade X>=0’0,X=<0’9
a notação 0’9 denota o valor ascii do caracter ”9”.
1
2
3
4
5
%% sem aç~
oes semanticas
digitos :- digito, rdigitos,!.
rdigitos :- digito, rdigitos,!.
rdigitos .
digito :- peek_code(X),X>=0’0,X=<0’9,get_code(L),!.
6
7
8
9
10
11
12
%% codificada com cláusulas recursivas
digitos(D) :- digito(L),rdigitos(Ls),atom_codes(D,[L|Ls]),!.
rdigitos([L|Ls]) :- digito(L),rdigitos(Ls),!.
rdigitos( [] ) .
digito(D) :- peek_code(D),D>=0’0,D=<0’9,get_code(D),!.
Na próxima seção mostramos esta técnica aplicada a um léxico para uma mini linguagem.
Um léxico trabalhando com um arquivo
A seguir apresentamos um gerador de tokens que trabalha com arquivos texto. Entra
um ”programa” representado num arquivo texto, como uma fita de entrada, e sai uma
lista (fita) de tokens, ver Figura 4.2.
Figura 4.2: Integração entre os componentes Léxico e Sintático.
A saı́da do Léxico é entrada para um componente sintático. Usualmente o componente
léxico devolve as palavras da linguagem e o componente sintático monta construções como
frases e comando a partir das palavras.
CAPÍTULO 4. PROGRAMAÇÃO DE GRAMÁTICAS EM PROLOG
81
Estudo de caso: Gerando tokens para uma mini linguagem
Vamos implementar um léxico para uma mini linguagem tipo Pascal, onde podemos
escrever programas como o exemplificado abaixo. Assumimos que este código Pascal está
em um arquivo, o qual será processado pelo léxico para gerar um lista de tokens.
1
2
3
4
5
6
7
8
9
program a1;
function pos(i:int):bool;
begin pos:=i>=0; end;
begin
readln(X1);
writeln(pos(X1));
XX := X1*X1;
writeln(XX);
end;
Na saı́da do léxico teremos uma lista de tokens. Vamos fazer três versões do léxico:
• um léxico simples, com propósitos didáticos - ele gera uma lista de tokens;
• um léxico que classifica as palavras em reservadas e que informa a linha e coluna
onde o token foi encontrado;
Gerando uma lista de tokens
A saı́da da primeira versão é mostrada abaixo. Os tokens são escritos (para efeito de
visualização e depuração do léxico) e ao mesmo tempo devolvidos em uma lista com os
tokens (para serem usados pelo componente sintático).
?- [tokens]. %% swi-prolog
% tokens compiled 0.00 sec, -632 bytes
Yes
?- ftokens(N).
program a1 ; function pos ’(’ i : int ’)’ : bool ; begin pos := i
= ’0’ ; end ; begin readln ’(’ ’X1’ ’)’ ; writeln ’(’ pos ’(’ ’X1’
’)’ ’)’ ; ’XX’ := ’X1’ * ’X1’ ; writeln ’(’ ’XX’ ’)’ ; end ;
N = [program, a1, (;), function, pos, ’(’, i, :, int|...]
Yes
Para trabalhar com um arquivo de entrada usamos as primitivas, do Prolog ISO, que
manipulam códigos de caracteres: peek_code/1, get_code/1. Com isso, definimos que
queremos trabalhar com os códigos Ascii dos caracteres1 .
1
Uma abordagem alternativa seria trabalhar diretamente com os caracteres usando as primitivas
peek char e get char. O problema é que para lermos de um arquivo devemos tratar também os caracteres que não são visı́veis. Portanto para não mesclar caracteres com códigos preferimos trabalhar só
com códigos.
CAPÍTULO 4. PROGRAMAÇÃO DE GRAMÁTICAS EM PROLOG
82
O predicado principal ftokens/1 abre o arquivo de entrada e informa que ele será a
fita de entrada set_input. Após, chama-se o léxico propriamente dito, tokensN/1, que
gera uma lista de tokens que é escrita pelo predicado wList.
1
2
3
4
5
6
ftokens(N) :- open(’teste.pl’,read,_,[type(text),alias(file_in)]),
set_input(file_in),
tokensN(N), wList(N),!,
close(file_in).
wList([L|Ls]):-!,writeq(L),write(’ ’),wList(Ls).
wList([])
:-!.
O predicado principal do léxico tokensN foi programado para trabalhar até o fim de
arquivo ser encontrado at_end_of_stream. Este predicado, tokensN, chama repetidamente o predicado separadores seguido do predicado token/1 que retorna um token.
É necessário sempre verificar se entre um token e outro existe ou não um separador.
Separadores (isSeparador) são os códigos menores que 32 e maiores que -1. O valor -1
é retornado pelas primitivas de leitura de códigos quando é encontrado o fim de arquivo.
Segue o programa que gera os tokens. Abaixo, comentaremos os pontos mais importantes deste programa.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
tokensN( []
)
tokensN([N|Ns])
token( N
)
separador
separadores
palavra( Po
)
::::::-
at_end_of_stream,!.
separadores, token(N),tokensN(Ns),!.
digitos(N),!; palavra(N),!; simbolo(N),!.
peek_code(C), isSeparador(C), get_code(C),!.
separador, separadores,!; true.
palavra0(N), atom_codes(P,N),
(pal_res(P),Po=res(P),!;Po=id(P),!).
palavra0([L|Ls]) :- letra(L), letOuDigs(Ls),!.
letOuDigs([L|Ls]) :- peek_code(L),isLetOuDig(L),get_code(L),letOuDigs(Ls),!.
letOuDigs( [] ) .
digito(L) :- peek_code(L),isDigito(L),get_code(L),!.
letra(L) :- peek_code(L),isLetra(L),get_code(L),!.
digitos(D) :- digito(L),rdigitos(Ls),atom_codes(D,[L|Ls]),!.
rdigitos([L|Ls]) :- digito(L),rdigitos(Ls),!.
rdigitos( [] ) .
%%
simbolo(S) :- peek_code(C1),isSimbolo(C1),get_code(C1),
peek_code(C2),simbolo2(C1,C2,S),!.
simbolo2(C1,C2,S) :- table2(C1,C2,S),get_code(C2),!;atom_codes(S,[C1]),!.
%%
table2( 0’:, 0’=, ’:=’ ).
table2( 0’<, 0’>, ’<>’ ).
table2( 0’<, 0’=, ’<=’ ).
CAPÍTULO 4. PROGRAMAÇÃO DE GRAMÁTICAS EM PROLOG
24
25
26
27
28
29
%%
isLetOuDig(C)
isLetra(C)
isDigito(C)
isSimbolo(C)
isSeparador(C)
:::::-
83
isLetra(C),!; isDigito(C),!.
C>=0’a,C=<0’z,!; C>=0’A,C=<0’Z,!; C=0’_,!.
C>=0’0,C=<0’9,!.
C>32, \+ isLetOuDig(C),!.
C> -1,C<33,!.
%% -1 = eof
Temos cinco predicados para classificar códigos em tipos de caracteres:
• isSeparador — valores menores ou iguais a 32.
• isSimbolo — valores maiores que 32, mas que não são letras e nem dı́gitos.
• isDigito — valores compreendendo os dı́gitos, sabemos que os dı́gitos estão no
intervalo entre 0’0 e 0’9. Esta notação zero com apóstrofo seguido de um caracter
representa o código do caracter.
• isLetra — define o conjunto das letras minúsculas, maiúsculas mais o ’underscore’.
• isLetOuDig — define o conjunto união das letras e dı́gitos.
A partir destes predicados que classificam os códigos são definidos três tipos de tokens:
inteiros, palavras e sı́mbolos.
A leitura de um código de caracter na fita de entrada é feita em dois passos: (1)
primeiro o peek_code verifica qual é o código sem avançar o cursor da fita; se o código
pertence ao conjunto testado, por exemplo, é um dı́gito, então a primitiva get_code
consome o caracter.
digito(L) :- peek_code(L),isDigito(L),get_code(L),!.
Esta técnica implementa um léxico que olha um caracter a frente (”scanner” com um
sı́mbolo de ”Lookahead”).
O predicado digitos chama repetidas vezes o digito, pelo menos uma. Ele pode ser
testado com uma pergunta como a segue.
?- digitos(N).
|
1222 ff <enter>
N = ’1222’
?- token(T).
|
aaaa2222 ooo <enter>
T = aaaa2222
?- token(T).
|
:= <enter>
T = :=
CAPÍTULO 4. PROGRAMAÇÃO DE GRAMÁTICAS EM PROLOG
84
Na verdade todos os predicados que não fazem referencia direta as operações de arquivo (e.g., at_end_of_stream ) podem ser testados diretamente com perguntas que
lêem do teclado: por exemplo, token, palavra, simbolo, etc. (tokensN usa o predicado
at_end_of_stream que faz referência a um arquivo).
Outro predicado que merece uma apresentação é o sı́mbolo, porque no Pascal alguns
sı́mbolos são formados pela concatenação de dois caracteres simbólicos, por exemplo: >=,
<> e :=. Assim, quando é reconhecido um sı́mbolo, ele é lido (get_char) em seguida
consultamos uma tabela, olhando o próximo caractere da fita com peek_code:
• caso o par (o lido, mais o lookahead) estejam em table2 então o lookahead também
é consumido (lido); a table2 devolve a concatenação dos dois caracteres;
• caso contrário, devolvemos o sı́mbolo lido, sem avançar o cursor (sem consumir o
próximo caracter).
Segue o código para reconhecer sı́mbolos de um ou dois caracteres. table2 codifica
as combinações válidas de sı́mbolos formados por dois caracteres.
simbolo(S) :- peek_code(C1),isSimbolo(C1),get_code(C1),
peek_code(C2),simbolo2(C1,C2,S),!.
simbolo2(C1,C2,S) :- table2(C1,C2,S),get_code(C2),!;atom_codes(S,[C1]),!.
4.2.3
Gerando palavras reservadas e números de linhas
Aqui apresentamos a segunda versão deste léxico. Agora, queremos algumas melhorias
no programa anterior. Segue abaixo a execução da nova versão. Note que cada token
é definido pelo functor tok/3. O token tok(1,1,res(program)) diz que na linha 1 e
coluna 1 foi encontrada a palavra reservada program; o token tok(6, 16, id(pos)) diz
que na linha 6 coluna 16 foi encontrado o id(entificador) pos, e assim por diante.
ftokens(N).
tok(1, 1, res(program)) tok(1, 9, id(a1)) tok(1, 11, (;))
tok(2, 4, res(function)) tok(2,13,id(pos)) tok(2,16, ’(’) tok(2,17, id(i))
tok(2,18,:) tok(2,19, res(int)) tok(2,22, ’)’) tok(2,23,:)
...
tok(6, 8, id(writeln)) tok(6, 15, ’(’) tok(6, 16, id(pos)) tok(6,19,’(’)
tok(6, 20, id(’X1’)) tok(6, 22, ’)’)
N = [tok(1, 1, res(program)), tok(1, 9, id(a1)), tok(1, 11, (;)),
tok(..., ..., ...)|...]
Para gerar os números de linha e coluna usamos dois predicados existentes no SWI-PROLOG,
respectivamente line_count/2 e line_position/2.
tokensN([tok(LC,LP,N)|Ns]) :- delims,
line_count(file_in,LC),line_position(file_in,LP),
token(N),tokensN(Ns),!.
CAPÍTULO 4. PROGRAMAÇÃO DE GRAMÁTICAS EM PROLOG
85
Estes predicados que retornam o número da linha e coluna podem ser facilmente
programados a partir dos predicados de leitura de códigos de caracteres. Toda vez que
a seqüência (13) (10) for encontrada é uma nova linha: então o valor de line_count
deve ser incrementado e o valor de line_position zerado. Para contar os caracteres
cria-se uma primitiva a partir da get_code para incrementar um contador de caracteres
line_position, toda vez que é chamada.
Por outro lado, para classificar as palavras, entre reservadas e identificadores, basta
criar uma tabela de palavras reservadas. Para cada linguagem existe apenas um número
finito e pequeno de palavras. Porém, o número de identificadores é potencialmente infinito
(ou muito grande). Após o reconhecimento de uma palavra (P) perguntamos se ela é
reservada, em caso afirmativo retornamos res(P) em caso negativo id(P).
palavra( Po
) :- palavra0(N), atom_codes(P,N),
(pal_res(P),Po=res(P),!;Po=id(P),!).
Segue abaixo a parte do programa que foi modificada, conforme descrito acima. No
inı́cio do código está a tabela de palavras reservadas. Note que para introduzir as melhorias desta versão modificamos apenas duas regras (o código que não é mostrada aqui, é o
mesmo da versão anterior).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pal_res(program).
pal_res(function).
pal_res(int).
pal_res(begin).
pal_res(end).
%%
tokensN( []
) :- at_end_of_stream,!.
tokensN([tok(LC,LP,N)|Ns]) :- separadores,line_count(file_in,LC),
line_position(file_in,LP), token(N),tokensN(Ns),!.
token( N
) :- digitos(N),!; palavra(N),!; simbolo(N),!.
separador :- peek_code(C), isSeparador(C), get_code(C),!.
separadores :- separador, separadores,!; true.
palavra( Po
) :- palavra0(N), atom_codes(P,N), (pal_res(P),Po=res(P),!;Po=id(P),!)
palavra0([L|Ls]) :- letra(L), letOuDigs(Ls),!.
letOuDigs([L|Ls]) :- peek_code(L),isLetOuDig(L),get_code(L),letOuDigs(Ls),!.
letOuDigs( [] ) .
digito(L) :- peek_code(L),isDigito(L),get_code(L),!.
letra(L) :- peek_code(L),isLetra(L),get_code(L),!.
digitos(D) :- digito(L),rdigitos(Ls),atom_codes(D,[L|Ls]),!.
rdigitos([L|Ls]) :- digito(L),rdigitos(Ls),!.
rdigitos( [] ) .
simbolo(S) :- peek_code(C1),isSimbolo(C1),get_code(C1),
peek_code(C2),simbolo2(C1,C2,S),!.
simbolo2(C1,C2,S) :- table2(C1,C2,S),get_code(C2),!;atom_codes(S,[C1]),!.
CAPÍTULO 4. PROGRAMAÇÃO DE GRAMÁTICAS EM PROLOG
86
Com esta abordagem podemos programar léxicos para qualquer tipo de processadores
de linguagem, tais como compiladores, como solicitado no exercı́cio que segue.
Exercı́cio 4.2.5 Usando a técnica apresentada, nesta seção, escreva um léxico para a
linguagem Pascal. Procure um manual de uma versão da linguagem que contenha a
descrição das palavras reservadas, dos operadores (+ − ∗/ :=<> ...) e dos tokens (identificadores, inteiros, reais e outras representações para números).
Capı́tulo 5
Programação de autômatos
5.1
Métodos de codificação de reconhecedores
Neste capı́tulo implementamos os métodos de codificação de gramáticas regulares(com
goto, iterativo, e recursivo), ou autômatos, nas linguagens Pascal, Java e C++. Em Java
só podemos codificar de duas formas pois não existe um comando goto.
As linguagens testes são {a∗ b∗ } ou {a∗ b+ }, conforme for conveniente. Para codificar a
versão com goto o ideal é usarmos uma gramática escrita na forma GLUD (ver capı́tulo
sobre gramáticas regulares), como segue:
1
2
a --> [a] a | [b] b | []
b --> [b] b | []
A versão iterativa, basicamente, codifica a expressão regular {a∗ b+ }; o uso desta
gramática, onde pelo menos um b é obrigatório, mostra como codificar um terminal
obrigatório. E, a versão recursiva codifica a gramática r abaixo; também, poderia ser a
gramática GLUD descrita acima.
1
2
3
r --> a b
a --> [a] a | []
b --> [b] b | []
Abaixo implementamos estes três métodos para cada uma das linguagens iniciando
por C++. Por fim, na última seção deste capı́tulo mostramos como contabilizar os
tempos (usando funções de leitura do relógio da máquina) para fazer comparações de
desempenho entre os diferentes métodos combinados com as diferentes linguagens formais
nas diferentes linguagens de programação.
5.1.1
Versão em C++
Na versão em C(C++) podemos usar uma macro #define visando otimizar o código,
pois a função xp pode ser escrita como uma macro com corpo (s[p]==c) e, de forma
87
CAPÍTULO 5. PROGRAMAÇÃO DE AUTÔMATOS
88
similar, a função np como uma macro de corpo (++p). Em C++, todo valor acima de
zero equivale a um valor verdadeiro – primeiro incrementamos o p, pois ele é inicialmente
zero (um string em C++ inicia na posição zero).
1
2
3
4
5
// auto_goto.cpp
#include <iostream.h>
#include <stdio.h>
#include <string.h>
#include <windows.h>
6
7
8
9
10
11
12
13
14
15
// ***begin PRIMITIVAS
char s[]=""; int p;
void le_palavra() {p=0; cout << "digite uma palavra:\n"; s << cin << "@"; }
bool xp(char c)
{return(s[p]==c);}
bool np()
{p++; return(true);} // ou {return(++p)}
//
// #define xp(c) (s[p]==c)
// #define np() (++p)
// ***end PRIMITIVAS
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void main()
// simula uma gramática GLUD
{
le_palavra();
{
p=0;
goto a;
a:
if (xp(’a’) && np()) goto a;
else if (xp(’b’) && np()) goto b;
else
goto final;
b:
if (xp(’b’) && np()) goto b;
else
goto final;
final: if (xp(’@’))
cout << "Reconheceu até: " << p;
else cout << "Erro na posiç~
ao: " << p;
}}
No programa abaixo temos a mesma gramática codificada como um autômato iterativo. Com o objetivo de economizar espaço, nos programas que seguem deixamos de repetir
o preâmbulo que inclui as assertivas #include e as funções primitivas que implementam
a fita de entrada.
1
2
3
4
// auto_iter.cpp
// simula uma express~
ao regular a* b+ = a* bb*
// includes & PRIMITIVAS
CAPÍTULO 5. PROGRAMAÇÃO DE AUTÔMATOS
5
6
7
8
9
10
89
void main()
{
le_palavra();
{
p=0;
while xp(’a’) && np();
if (xp(’b’) && np())
{ while xp(’b’) && np();
11
if (xp(’@’))
cout << "Reconheceu até: " << p;
else cout << "Erro na posiç~
ao: " << p;
12
13
14
}
else cout << "Erro na posiç~
ao: " << p;
15
16
17
}}
Por fim temos a versão do autômato com regras recursivas. Normalmente, por questão
de eficiência as versões não recursivas são preferidas, porém, como veremos numa análise
comparativas destes três métodos, o tempo não é tão diferente (tipicamente o dobro)
portanto o programador pode escolher qualquer uma das abordagens, ciente das suas
limitações.
1
2
3
// auto_recur.cpp
// linguagem a*b*
// includes & PRIMITIVAS
4
5
6
7
bool A() { return(xp(’a’) && np() && A() || true); }
bool B() { return(xp(’b’) && np() && B() || true); }
bool R() { return(A() && B()); }
8
9
10
11
12
13
14
15
void main()
{
le_palavra();
{
p=0;
if (R() && xp(’@’))
cout << "Reconheceu até: " << p;
else cout << "Erro na posiç~
ao: " << p;
}}
O método recursivo é o mais próximo da notação gramatical. Por isso, podemos
afirmar que ele é o mais declarativo. O método do goto é bem próximo da notação
gráfica do automato. E o método iterativo é mais próximo das espressões regulares.
Exercı́cio 5.1.1 Modifique uma ou mais linhas da versão C++ do programa com goto,
para implementar a linguagem a∗ b+ .
Exercı́cio 5.1.2 Modifique uma ou mais linhas da versão C++ do programa iterativo,
para implementar a linguagem a∗ b∗ .
CAPÍTULO 5. PROGRAMAÇÃO DE AUTÔMATOS
90
Exercı́cio 5.1.3 Modifique uma ou mais linhas da versão C++ do programa recursivo,
para implementar a linguagem a∗ b+ .
5.1.2
Versão em Pascal
Segue abaixo os códigos em Pascal. Note que um string em Pascal inicia na posição
um.
1
2
program auto_goto;
uses crt,WinDos;
3
4
5
6
7
8
9
10
11
12
13
14
{--begin PRIMITIVAS -----}
type bool=boolean;
const fim = ’@’;
var
s:string; s := ’’;
var
p:integer;
procedure le_palavra;
begin clrscr; p:=1; write(’Digite uma palavra:’); readln(s); s:=s+fim;
end;
function xp(c: char):bool; begin x:= s[p]=c; end;
function np
:bool; begin p:= p+1; n:=true; end;
{--end PRIMITIVAS---}
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
label a, b, final;
begin
le_palavra;
begin
p:=1;
goto a;
a:
if xp(’a’) and np then goto a
else if xp(’b’) and np then goto b
else
goto final;
b:
if xp(’b’) and np then goto b
else
goto final;
final: if xp(fim) then writeln(’Reconheceu até: ’, p)
else writeln(’Erro na posicao: ’, p);
end;
end.
As versões em Pascal em C++ são quase idênticas, pois as duas linguagens possuem
as mesmas construções, com pequenas variações na sintaxe. Inclusive o número de linhas
no corpo principal do programa é o mesmo.
CAPÍTULO 5. PROGRAMAÇÃO DE AUTÔMATOS
1
2
3
4
91
program auto_iter;
{- linguagem a*b+ -}
uses crt,WinDos;
{- include PRIMITIVAS -}
5
6
7
8
9
10
11
12
13
14
15
16
17
18
begin
le_palavra;
begin
p:=1;
while xp(’a’) and np do;
if xp(’b’) and np
then begin
while xp(’b’) and np do
if xp(fim)
then writeln(’Reconheceu até:’, p)
else writeln(’Erro na posiç~
ao:’, p);
end
else writeln(’Erro na posiç~
ao: ’,p);
19
20
21
end;
end.
Num programa recursivo sobre gramáticas, em Pascal, é bom declarar todas funções
recursivas com a diretiva forward (significando que a função será definida mais adiante
no texto). Isto facilita a definição das funções em qualquer ordem dentro do texto do
programa, sem causar erros de sintaxe.
1
2
program auto_recur;
{- include PRIMITIVAS -}
3
4
5
6
7
8
9
function
function
function
function
function
function
R:
A:
B:
R:
A:
B:
bool;
bool;
bool;
bool;
bool;
bool;
forward;
forward;
forward;
begin R:= A and B; end;
begin A:= xp(’a’) and np and A or true; end;
begin B:= xp(’b’) and np and B or true; end;
10
11
12
13
14
15
16
begin
le_palavra;
begin
p:=1;
if R and xp(fim) then writeln(’Reconheceu até:’, p)
else writeln(’Erro na posiç~
ao:’, p);
CAPÍTULO 5. PROGRAMAÇÃO DE AUTÔMATOS
17
18
92
end;
end.
Exercı́cio 5.1.4 Modifique uma ou mais linhas da versão Pascal do programa com goto,
para implementar a linguagem a∗ b+ .
Exercı́cio 5.1.5 Modifique uma ou mais linhas da versão Pascal do programa iterativo,
para implementar a linguagem a∗ b∗ .
Exercı́cio 5.1.6 Modifique uma ou mais linhas da versão Pascal do programa recursivo,
para implementar a linguajem a∗ b+ .
5.1.3
Versão em Java
Segue abaixo as versões em Java. Java não tem o comando goto. Portanto, temos só
duas implementações: iterativo e recursivo.
1
2
3
4
5
6
7
8
9
10
import java.util.*;
public class IfWhile
{
// ---begin PRIMITIVAS
private static String s;
private static int p;
//
private static void le_palavra()
{p=0;s="aaaaaaabbbbbbbbbb@";}
private static boolean xp(String c){return(s.substring(p,p+1).equals(c));}
private static boolean np()
{p++;return(true);}
// ---end PRIMITIVAS
11
12
13
14
15
16
17
18
19
20
21
22
public static void main(String args[])
{
String fim = "@";
le_palavra();
{
p=0;
while (xp("a") && np());
if (xp("b") && np() )
{ while (xp("b") && np());
if (xp(fim))
System.out.println("Reconheceu até: " + (p+1));
else System.out.println("Erro na posiç~
ao: " + (p+1));
}}}}
Esta versão do método recursivo em Java usa as mesmas funções primitivas do método
iterativo.
CAPÍTULO 5. PROGRAMAÇÃO DE AUTÔMATOS
93
1
2
3
4
5
6
import java.util.*;
public class Recursao
{private static boolean R() {return(A() && B()); }
private static boolean A() {return(xp("a") && np() && A() || true);}
private static boolean B() {return(xp("b") && np() && B() || true);}
7
8
9
10
11
12
13
14
15
public static void main(String args[])
{
String fim = "@";
le_palavra();
{
p=0;
if (R() && xp(fim))
System.out.println("Reconheceu até: ", p);
else System.out.println("Erro na posiç~
ao: " + (p+1));
}}}
Exercı́cio 5.1.7 Modifique uma ou mais linhas da versão Java do programa com iterativo, para implementar a linguagem a∗ b∗ .
Exercı́cio 5.1.8 Modifique uma ou mais linhas da versão Java do programa com recursiva, para implementar a linguajem a∗ b+ .
5.2
Contagem de tempo
O programa abaixo implementa uma versão de autômato com goto em C++, contanto
o tempo de execução. A constante max = 10000 define o número de vezes que devemos
rodar o programa principal para podermos ter um valor de tempo em milisegundos visı́vel
(diferente de zero).
Dois procedimentos savetime e showtime são usados respectivamente antes e depois
do corpo principal do programa. É importante salvar o tempo somente após a leitura ou
inicialização da fita de entrada. Para comparar dois algoritmos, em termos de performance, devemos usar a mesma fita de entrada além de codificar as produções da gramática
na mesma ordem, nas diferentes linguagens. Portanto, é melhor inicializar uma fita de
comprimento 100. Porém, para testar se o algoritmo está funcionando, é melhor poder
ler fitas menores digitadas diretamente no teclado.
Na versão em C(C++) podemos usar a macro #define para otimizar a performance,
como comentamos anteriormente.
Além disso, devemos comentar a linha que escreve se reconheceu ou não, pois sabemos
que a fita será reconhecida. Uma escrita na tela anularia totalmente a leitura do tempo
de processamento.
CAPÍTULO 5. PROGRAMAÇÃO DE AUTÔMATOS
5.2.1
94
Versão em C++
Segue uma versão com contagem de tempo em C++. No final da fita de entrada
acrescentamos um carácter "@" (como token) sinalizando fim da fita. Após o reconhecimento da sentença testamos se é o fim da fita. Se ainda não estamos no fim da fita é uma
situação de erro.
1
2
// auto_goto.cpp
// gramática regular a* b*
3
4
5
6
7
#include
#include
#include
#include
<iostream.h>
<stdio.h>
<string.h>
<windows.h>
8
9
10
11
12
DWORD start, tempo;
void savetime() {start = ::GetTickCount();}
void showtime() {tempo = ::GetTickCount() - start;
cout << "Tempo gasto 1/1000 seg:" << tempo;}
13
14
15
// ***begin PRIMITIVAS
int p; char s[]="aaaaaaaaaaaaaaaaaaaaa...bbbbbbbbbbbbbb@"; // 50as e 50bs
16
17
18
19
void le_palavra() { p=0; }
bool xp(char c)
{return(s[p]==c);}
bool np()
{p++; return(true);}
// ou {return(++p)} pois p começa em 0
20
21
22
// #define xp(c) (s[p]==c)
// #define np() (++p)
23
24
// ***end PRIMITIVAS
25
26
const max=10000; int i;
27
28
void main()
29
30
31
32
33
34
35
36
37
{
le_palavra();
savetime();
for(int i=0;i<=max;i++)
{
p=0;
goto a;
a:
if (xp(’a’) && np()) goto a;
else if (xp(’b’) && np()) goto b;
else
goto final;
CAPÍTULO 5. PROGRAMAÇÃO DE AUTÔMATOS
b:
if (xp(’b’) && np()) goto b;
else
goto final;
final: if (xp(’@’))
;// cout << "Reconheceu até: " << p;
else cout << "Erro na posicao: " << p;
38
39
40
41
42
}
showtime();
43
44
45
95
}
5.2.2
Versão em Pascal
Em Pascal o tempo é medido a partir da função GetTime(th,tm,ts,tc) onde estas
variáveis trazem os tempos em hora, minuto, segundo e centésimo de segundo. Assim
quando é feita a contagem do tempo devemos multiplicar os centésimos de segundos por
10; os segundos por 1000; e, os minutos por 60000; com isso obtemos um único valor
normalizado.
1
2
program auto_goto;
uses crt,WinDos;
3
4
5
6
7
8
9
10
11
12
{--begin TEMPO-----------}
var
th,tm2,tm,ts2,ts,tc2,tc: Word;
procedure saveTime; begin GetTime(th,tm, ts, tc); end;
procedure showTime; begin GetTime(th,tm2,ts2,tc2);
writeln( ’Tempo 1/1000 seg:’,
(tm2-tm)*60000+(ts2-ts)*1000+(tc2-tc)*10);
Repeat until keypressed;
end;
{--end TEMPO------------}
13
14
15
16
17
18
{--begin PRIMITIVAS ----}
type bool=boolean;
const fim = ’@’;
var s:string;
var p:integer;
19
20
21
22
23
procedure le_palavra;
begin p :=1;
s := ’aaaaaaaaaaaaaaaaaaaaaa....bbbbbbbbbbbbbbbbbbbbbbbbbb@’; {50a 50b}
end;
24
25
26
27
function xp(c: char):bool; begin x:= s[p]=c; end;
function np
:bool; begin p:= p+1; n:=true; end;
{--end PRIMITIVAS ----}
CAPÍTULO 5. PROGRAMAÇÃO DE AUTÔMATOS
96
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
begin
const max=10000;
var
i :integer;
label a, b, final;
le_palavra;
saveTime;
for i := 0 to max do
begin
p:=1;
goto a;
a:
if xp(’a’) and np then goto a
else if xp(’b’) and np then goto b
else
goto final;
b:
if xp(’b’) and np then goto b
else
goto final;
final: if xp(fim) then {writeln(’Reconheceu até: ’, p)}
else writeln(’Erro na posicao: ’, p);
end;
showTime;
end.
5.2.3
Versão em Java
A linguagem Java possui, como o C++, uma função que lê o relógio em milisegundos,
currentTimeMillis; isto facilita a programação da contagem do tempo.
1
import java.util.*;
2
3
4
5
6
7
public class IfWhile
{
private static long start, tempo;
private static void savetime()
{ start = System.currentTimeMillis(); }
8
9
10
11
private static void showtime()
{ tempo = System.currentTimeMillis() - start;
System.out.println("Tempo gasto 1/1000 segundos: " + tempo);}
12
13
14
15
// ---begin PRIMITIVAS
private static String s;
private static int p;
16
17
private static void le_palavra()
CAPÍTULO 5. PROGRAMAÇÃO DE AUTÔMATOS
18
19
{
97
p=0;
s="aaaaaaaaaaaaaaaaaaaaaaaaaaaaa...bbbbbbbbbbbbbbbbbbbbbbbb@"; }
20
21
22
private static boolean xp(String c)
{
return(s.substring(p,p+1).equals(c)); }
23
24
25
26
private static boolean np()
{
p++;return(true); }
// ---end PRIMITIVAS
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public static void main(String args[])
{
int max = 10000;
le_palavra();
savetime();
for (int i=0;i<=max;i++)
{
p=0;
while (xp("a")) np();
if (xp("b"))
{ while (xp("b")) np();
if (xp("@"))
;// System.out.println("Reconheceu até:", p);
else System.out.println("Erro na posicao: " + (p+1));
}}
showtime();
}}
Vimos um exemplo de programa de contagem de tempo para cada linguagem. Alguns
destas funções dependem de certas bibliotecas (ou unidades) de certos compiladores. Caso
elas não estejam disponı́veis, o leitor deverá buscar no seu compilador outras funções de
leitura do tempo que sejam similares.
Segue abaixo um mini projeto para fazer uma análise comparativa entre os diferentes
métodos para as diferentes linguagens.
Exercı́cio 5.2.1 Projeto: Faça um programa que gera na saı́da uma tabela com um
comparativo para os tempos das diferentes versões de programas apresentados. Como
está esquematizado abaixo (os valores de tempos são apenas ilustrativos). Após obter os
tempos em seu computador, e com a sua experiência, responda:
• Qual o método (goto, iterativo ou recursivo) mais eficiente independente de linguagem de programação?
• Qual é a linguagem de programação mais eficiente para a codificação destes métodos?
• Qual é o método mais simples de ser codificado ?
CAPÍTULO 5. PROGRAMAÇÃO DE AUTÔMATOS
Num Pentium VII 5Gmhz,
para fitas de 100 caracteres,
para um ciclo de 10000 vezes, tempos em milisegundos:
em C++ com goto : 300 ms
& com #define :
260 ms
em C++ iterativo : 270 ms
& com #define :
240 ms
em C++ recursivo : 340 ms
& com #define :
280 ms
em Pascal com goto : 400 ms
em Pascal iterativo : 470 ms
em Pascal recursivo : 440 ms
em Java
iterativo : 10470 ms
em Java
recursivo : 10440 ms
98
Capı́tulo 6
Programação de Gramáticas Livres
de Contexto
Neste capı́tulo apresentamos a programação de gramáticas livres de contexto nas três
linguagens alvo: Pascal, Java e C++.
São apresentados três métodos de programação: o primeiro para uma gramática LL(1)
fatorada, implementando um reconhecedor descendente recursivo sem retrocesso (DRSR).
Os outros dois métodos implementam um reconhecedor descendente recursivo com retrocesso: (1) de modo procedural salvando o ponteiro sobre a fita de entrada e (2) simulando
a implementação de uma gramática de cláusulas definidas (DCG) – também chamado de
método da costura.
6.1
Versão em Java
A linguagem para testes é {an bn }. Segue a implementação da versão fatorada pela
gramática abaixo.
1
2
l --> [a], l1.
l1 --> l, [b] | [b].
Aqui utilizaremos as duas variantes do método descendente recursivo (com e sem
recursividade), que já foi apresentado no capı́tulo anterior.
Segue a versão DRSR (sem retrocesso).
1
2
3
4
5
6
7
import java.util.*;
public class DRSR
{
// include PRIMITIVAS
//
private static boolean L()
{return(xp("a") && np() && L1());}
private static boolean L1()
99
CAPÍTULO 6. PROGRAMAÇÃO DE GRAMÁTICAS LIVRES DE CONTEXTO 100
{return((L() && xp("b")) && np()) || (xp("b") && np());}
8
//
public static void main(String args[])
{ le_palavra();
{
p=0;
if (L() && xp("@"))
System.out.println("Reconheceu até: " + p );
else System.out.println("Erro na posicao: " + (p+1));
}}}
9
10
11
12
13
14
15
16
Para o método DRCR, usamos a versão, dada abaixo, não fatorada da gramática.
1
2
l --> [a], l, [b].
l --> [a], [b].
Na implementação desta versão da gramática, devemos nos preocupar em retroceder
o ponteiro sobre a fita de entrada toda vez que a primeira alternativa falha, quando é
tentada a próxima alternativa.
O ideal é sempre fatorarmos a gramática. Porém, existem situações que a fatoração
é difı́cil especialmente para gramáticas de atributos, onde durante a fatoração somos
obrigados a reescrever as equações semânticas.
1
2
import java.util.*;
public class DRCR_salvando_p
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
// includes & PRIMITIVAS
private static boolean L()
{
int salvap = p;
if(xp("a") && np() && xp("b") && np()) return(true);
else
{ p = salvap;
if(xp("a") && np() && L() && xp("b") && np()) return(true);
else return(false);
}}
public static void main(String args[])
{
le_palavra();
{
p=0;
if (L() && xp("@"))
System.out.println("Reconheceu até: " + p);
else System.out.println("Erro na posicao: " + (p+1));
}}}
CAPÍTULO 6. PROGRAMAÇÃO DE GRAMÁTICAS LIVRES DE CONTEXTO 101
Em Java, o método da costura não tem uma codificação limpa como em Pascal e
C++, pois Java não permite através de um parâmetro retornar um valor inteiro (por
exemplo, i, i1); termos que retornar um objeto (int[] i), onde o valor inteiro está na
primeira posição do vetor, i[0]. Uma valor inteiro i pode entrar na função; apesar disso,
preferimos trabalhar com outro vetor para facilitar as atribuições.
No método da costura não é utilizado um ponteiro global, sobre a fita de entrada. O
ponteiro é implementado como um parâmetro nas funções. Assim, ele pode retroceder
automaticamente. Para isso a função xp, substituindo a função x, é utilizada. Se o valor
casa então ela aumenta uma posição no valor local do ponteiro sobre a fita de entrada.
Quando acontece um retrocesso, temos um novo problema que é como indicar a posição
de um erro. O ideal é mostar a posição mais avançada sobre a fita de entrada que já foi
utilizada. Para isso é utilizada uma variável global pe (posição do erro) que é atualizada
sempre que avançamos sobre um token da fita de entrada. Quando ocorre um retrocesso
pe se mantém, com o uso do comando: if (pe[0] < o[0]) {pe = o};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.*;
public class DRCR_com_costura
{// include PRIMITIVAS
private static int[] pe={o};
//
private static boolean xp1(String c, int[] i, int[] o)
{
o[0] = i[0] + 1; if (pe[0] < o[0]) {pe = o};
return(s.substring(i[0],i[0]+1).equals(c[0]));}
private static boolean L(int[] i, int[] o)
{
int[] i1 = {0}; int[] i2 = {0};
//
return(
xp1("a",i,i1) && L(i1,i2) && xp1("b",i2,o)
|| xp1("a",i,i1)
&& xp1("b",i1,o));}
//
public static void main(String args[])
{
int[] o = {0}; int[] o1 = {0};
le_palavra();
{
pe[0]=0;
int[] num = {0};
if (L(num,o) && xp1("@",o,o1))
System.out.println("Reconheceu até: " + o);
else System.out.println("Erro na posicao: " + (pe[0]+1));
}}}
6.2
Versão em C++
Segue a versão em C++ da gramática sem retrocesso (DRSR). A codificação segue
os mesmos princı́pios da versão em Java.
CAPÍTULO 6. PROGRAMAÇÃO DE GRAMÁTICAS LIVRES DE CONTEXTO 102
1
2
3
4
5
6
7
8
9
10
11
12
// DRSR
// includes & PRIMITIVAS
//
bool L() { return(xp(’a’) && np() && L1());
bool L1(){ return((L() && xp(’b’)) && np())
void main()
{ le_palavra();
{
p=0;
if (L() && xp(’@’))
cout << "Reconheceu até: " <<
else cout << "Erro na posicao: " <<
}}
}
|| (xp(’b’) && np()); }
p ;
(p+1);
Segue a solução em C++ da versão que salva o ponteiro para executar retrocesso
(DRCR/salvando).
1
2
// DRCR salva P
// includes & PRIMITIVAS
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool L()
{
int salvap = p;
if(xp(’a’) && np() && xp(’b’) && np())
return(true);
else
{
p = salvap;
if(xp(’a’) && np() && L() && xp(’b’) && np())
return(true);
else return(false);
}}
void main()
{
le_palavra();
{
p=0;
if (L() && xp(’@’))
cout << "Reconheceu ate: " << p ;
else cout << "Erro na posicao: " << (p+1);
}}
Exercı́cio 6.2.1 Modifique a solução que salva o p em C++, para indicar de forma correta a posição do erro, utilizando uma variável pe. Teste para algumas palavras inválidas.
A solução abaixo, do método DRCR, possui uma variável global pe indicando a posição
do erro.
CAPÍTULO 6. PROGRAMAÇÃO DE GRAMÁTICAS LIVRES DE CONTEXTO 103
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// DRCR com costura
int pe = 0;
bool xp1(char c, int i, int &o)
{ o = i + 1; if (pe < o){pe = o}; return(s[i]==c); }
//
bool L(int i, int &o)
{
int i1, i2;
return(
xp1(’a’,i,i1) && L(i1,i2) && xp1(’b’,i2,o)
|| xp1(’ba,i,i1) && xp1(’b’,i1,o)
);
}
void main()
{ int o, o1 = 0;
le_palavra();
{
pe =0;
if (L(1,o) && xp1(fim,o,o1))
cout << "Reconheceu ate: " << p;
else cout << "Erro na posicao: " << pe;
}}
6.3
Versão em Pascal
Nesta seção temos os três métodos codificados no Pascal. Segue o método DRSR.
1
2
3
4
5
program DRSR;
uses crt,WinDos;
...
function L:bool; begin L:=(xp(’a’) and np and L1()); end;
function L1:bool; begin L1:=(L() and xp(’b’) and np()) or (xp(’b’) and np()); end;
6
7
8
9
10
11
12
13
14
15
begin
le_palavra;
begin
p:=1;
if xp(L and xp(fim))
then writeln(’Reconheceu ate: ’, p)
else writeln(’Erro na posicao: ’, p);
end;
end.
Segue abaixo o método salva ponteiro. Este método salvando p é procedural, enquanto
que o método da costura é declarativo (similar as produções).
CAPÍTULO 6. PROGRAMAÇÃO DE GRAMÁTICAS LIVRES DE CONTEXTO 104
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
program DRCR_salvando_p;
uses crt,WinDos;
...
function L:bool;
var salvap: integer;
begin
salvap:=p;
if (xp(’a’) and np and xp(’b’) and n) then L:=true
else begin
p:=salvap;
if (xp(’a’) and np and L and xp(’b’) and n) then L:=true
else L:=false;
end;
end;
begin
le_palavra;
begin
p:=1;
if (L and xp(fim))
then {writeln(’Reconheceu’)}
else writeln(’Erro na posicao: ’,p);
end;
end.
Segue abaixo o método da costura (DRCR). Aqui implementamos o pe, para salvar a
posição do erro.
1
2
3
4
5
6
program DRCR_com_costura;
uses crt,WinDos;
...
var pe:integer;
function xp1(c: char; i:int; var o:int):bool;
begin o:=i+1; if pe<o then pe:=o; x1:=(s[o]=c); end;
7
8
9
10
11
12
13
function L(i:int; var o:int):bool;
var i1: int; i2: int;
begin
L:= xp1(’a’,i,i1) and L(i1,i2) and xp1(’b’,i2,o) or
xp1(’a’,i,i1) and xp1(’b’,i2,o);
end;
14
15
16
begin
le_palavra;
CAPÍTULO 6. PROGRAMAÇÃO DE GRAMÁTICAS LIVRES DE CONTEXTO 105
17
18
19
20
21
22
23
begin
pe := 0;
if (L(num,o) and xp1(fim,o,o1))
then {writeln(’Reconheceu até: ’,pe)}
else writeln(’Erro na posicao: ’,pe);
end;
end.
Exercı́cio 6.3.1 Modifique a solução que salva o ponteiro em Pascal para indicar de
forma correta a posição do erro, utilizando uma variável pe. Teste para algumas palavras
inválidas, tais como: aa@, bb@, abb@, aab@, etc. Teste estas palavras também com a
versão sem o pe. Qual é a importância do pe.
Exercı́cio 6.3.2 Projeto: Faça um programa que gera na saı́da uma tabela com um
comparativo para os tempos das diferentes versões de programas apresentados. Como
está esquematizado abaixo (os valores de tempos são apenas ilustrativos). Após obter os
tempos em seu computador, e com a sua experiência, responda:
• Qual o método mais eficiente independente de linguagem de programação?
• Qual é a linguagem de programação mais eficiente para a codificação destes métodos
para gramáticas?
• Qual é o método mais simples de ser codificado ?
Num Pentium VII 5Gmhz,
variacoes do metodo descendente recursivo (DR)
para fitas de 100 caracteres,
para um ciclo de 10000 vezes, tempos em milisegundos:
em C++
DRSR
: 300 ms
& com #define :
em C++
DRCR (salva p) : 270 ms
& com #define :
em C++
DRCR (costura) : 340 ms
& com #define :
em Pascal DRSR
: 400 ms
em Pascal DRCR (salva p) : 470 ms
em Pascal DRCR (costura) : 440 ms
em Java
DRSR
: 10470 ms
em Java
DRCR (salva p) : 10440 ms
em Java
DRCR (costura) : 10440 ms
260 ms
240 ms
280 ms
Capı́tulo 7
Programação de Gramáticas de
Atributos
Como exemplo de programação de gramáticas de atributos, codificamos inicialmente
a gramática {an bn cn } a partir da gramática livre de contexto {a∗ b∗ c∗ }. Um atributo
é usado para contar o número de as, bs e de cs. Uma equação é usada para forçar a
igualdade.
Segue a DCG correspondente ao programa.
1
2
3
4
5
6
7
r -->
a(N1)
a( 0)
b(N1)
b( 0)
c(N1)
c( 0)
a(X), b(Y), c(Z), {X=Y, Y=Z}.
--> [a],a(N), {N1:=N+1}.
--> [].
--> [b],b(N), {N1:=N+1}.
--> [].
--> [c],c(N), {N1:=N+1}.
--> [].
O atributo N1 é do tipo sintetizado, os valor do nó N filho somado com um é atribuı́do
ao nó pai N1 (se construirmos a árvore sintática).
7.1
Versão em Pascal
O atributo sintetizado é codificado, em cada função, pela variável n. Como em Pascal
podemos escrever sobrepor o valor de uma variável, por exemplo, em n:=n+1 , então não
é necessário utilizar uma variável n1.
A atribuição dentro da expressão lógica é implementada pela função attr, que avalia
uma expressão devolvendo o valor resultante.
Uma gramática de atributos é implementada sobre uma gramática livre de contexto.
Abaixo, usamos o método DRSR é usado para codificar a gramática livre de contexto.
106
CAPÍTULO 7.
1
PROGRAMAÇÃO DE GRAMÁTICAS DE ATRIBUTOS
107
program DRSR_GA;
2
3
uses crt, WinDos, fita;
4
5
6
function attr(var v:integer;exp:integer):bool;
begin v:=exp; attr:=true; end;
7
8
9
10
function A(var n: integer):bool;
function B(var n: integer):bool;
function C(var n: integer):bool;
11
12
13
14
15
16
17
function A; begin A:=(x(’a’) and np and A(n) and attr(n,n+1) or
true
and attr(n,0));
end;
function B; begin B:=(x(’b’) and np and B(n) and attr(n,n+1) or
true
and attr(n,0));
end;
function C; begin C:=(x(’c’) and np and C(n) and attr(n,n+1) or
true
and attr(n,0));
end;
18
19
20
21
22
23
24
function S:bool;
var na,nb,nc: integer;
begin
na :=0; nb:=0; nc:=0;
S:=(A(na) and B(nb) and C(nc) and (na=nb) and (nb=nc));
end;
25
26
27
28
29
30
31
32
33
34
35
begin
le_palavra;
begin
p[0]:=0;
if S and x(fim)
then write(’Reconheceu ate:’, p)
else if x(fim) then writeln(’Provavel erro nos valores de n’)
else writeln(’Erro na posicao: ’,p+1);
end;
end.
No corpo principal do programa temos dois comandos condicionais: o primeiro testa
se a palavra foi reconhecida, enquanto que o segundo comando testa se sintaticamente
chegamos até o fim, porém falhou uma das equações.
7.2
Versão em C(C++)
Segue o mesma gramática de atributos em C++.
CAPÍTULO 7.
1
2
PROGRAMAÇÃO DE GRAMÁTICAS DE ATRIBUTOS
// DRSR GA
// includes & PRIMITIVAS
3
4
5
char s[]="aaaaaaaa...bbbbbbbb...ccccccc@";
int p[] = {0};
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
bool attr(int &v,int exp) { v= exp; return(true);}
bool A(int &n)
{ return(x(’a’) && np() && A(n) && attr(n,n+1)
|| true
&& attr(n,0)); }
bool B(int &n)
{ return(x(’b’) && np() && B(n) && attr(n,n+1)
|| true
&& attr(n,0)); }
bool C(int &n)
{ return(x(’c’) && np() && C(n) && attr(n,n+1)
|| true
&& attr(n,0)); }
bool S()
{
int na, nb, nc;
return(A(na) && B(nb) && C(nc) && (na==nb) && (nb==nc)); }
void main()
{
le_palavra();
{
p[0]=0;
if (S() && x(’@’))
cout << "Reconheceu ate:", p;
else if (x(fim)) cout << "Erro nos valores de n" << p;
else
cout << "Erro na posicao: " << p;
}
7.3
Versão em Java
Idem, agora, em Java.
1
import java.util.*;
2
3
4
5
public class DRSR_GA
{
// includes & PRIMITIVAS
6
7
8
private static String s;
private static int[] p = {0};
9
10
private static void le_palavra()
108
CAPÍTULO 7.
PROGRAMAÇÃO DE GRAMÁTICAS DE ATRIBUTOS
109
{ s = "aaaaaaaaaaaaaaaaaaaaaa..bbbbbbbbb...ccccccccccccccccccc@"; }
11
12
private static boolean x(String c)
{ return(s.substring(p[0],p[0]+1).equals(c)); }
13
14
15
private static boolean np()
{ p[0]++; return(true); }
16
17
18
private static boolean attr(int[] v,int exp)
{ v[0] = exp; return(true); }
19
20
21
private static boolean A(int[] n)
{ return(x("a") && np() && A(n) && attr(n,n[0]+1)
|| true &&
attr(n,0));
}
22
23
24
25
private static boolean B(int[] n)
{ return(x("b") && np() && B(n) && attr(n,n[0]+1)
|| true
&& attr(n,0)); }
26
27
28
29
private static boolean C(int[] n)
{ return(x("c") && np() && C(n) && attr(n,n[0]+1)
|| true
&& attr(n,0)); }
30
31
32
33
private static boolean S()
{
int[] na = {0};
int[] nb = {0};
int[] nc = {0};
return(A(na) && B(nb) && C(nc) && (na[0]==nb[0]) && (nb[0]==nc[0]));
}
public static void main(String args[])
{
le_palavra();
{
p[0]=0;
if (S() && x("@"))
System.out.println("Reconheceu ate:", p);
else if (x(fim))
System.out.println("Provavel erro nos valores de n");
else System.out.println("Erro na posicao: " + (p[0]+1));
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
}}
Exercı́cio 7.3.1 Programe uma versão da gramática de atributos que segue, para a linguagem {an bn }. Use o método da costura; portanto, são utilizados dois parâmetros para
o método da costura mais um parâmetro para contar o número de as e bs. Faça nas três
linguagens de programação.
CAPÍTULO 7.
1
2
3
4
5
r -->
a(N1)
a(N1)
b(N1)
b(N1)
PROGRAMAÇÃO DE GRAMÁTICAS DE ATRIBUTOS
a(N),b(M),
--> [a],a(N),
--> [a],
--> [b],b(N),
--> [b],
110
{N=M}.
%equaç~
ao
{N1:=N+1}.
{N1:=1 }.
{N1:=N+1}.
{N1:=1 }.
Exercı́cio 7.3.2 Projeto: Faça um programa que gera na saı́da uma tabela com um
comparativo para os tempos das diferentes versões de programas apresentados. Como
está esquematizado abaixo (os valores de tempos são apenas ilustrativos). Após obter os
tempos em seu computador, e com a sua experiência, responda:
• Qual é a linguagem de programação mais eficiente para a codificação de uma GA?
• Qual é a versão mais simples e mais complicada?
Num Pentium VII 5Gmhz,
variacoes do metodo descendente recursivo (DR)
para fitas de 100 caracteres,
para um ciclo de 10000 vezes, tempos em milisegundos:
em C++
GA : 300 ms
em Pascal GA : 400 ms
em Java
GA : 10440 ms
Capı́tulo 8
Exerı́cios e Projetos de Programação
Introdução
Aqui apresentamos alguns exercı́cios e mini-projetos para exercitar as técnicas apresentadas nos capı́tulos anteriores.
O ideal é apresentar uma especificação para cada exercı́cio ou projeto: cada produção
de uma gramática deve resultar numa linha de um programa que a implementa. Com
isso podemos facilmente alterar a especificação do problema (a gramática) e em seguida
modificar o código da implementação.
8.1
Programação de Gramáticas
Exercı́cio 8.1.1 (MINI-PROJETO) Abaixo temos uma descrição para identificadores,
inteiros (também na notação hexadecimal) e reais. Com base nesta descrição responda
as perguntas abaixo.
1. Faça as produções para descrever os números haxadecimais;
2. Faça expressões regulares para: (1) identificadores; (2) inteiros; (3) reais; (4) hexadecimais;
3. Faça um autômato que possa reconhecer todos os tokens;
4. Programe o autômato, com o método GOTO;
5. Programe as expressões regulares com o método IF-WHILE; integre todos os fragmentos de programa num código único;
6. Programe a gramática regular com o método DRSR – note que as produções devem
estar fatoradas;
1
2
3
%% aaa123e3 aaaa | 122 | 0x23FF 0xaaaa | 123.3 10.20e23
token --> ident | inteiro | inteiroHexa | real
ident --> letra letraOuDigito
111
CAPÍTULO 8. EXERÍCIOS E PROJETOS DE PROGRAMAÇÃO
4
5
6
7
8
9
10
11
12
112
letra --> a..z A..Z
digito --> 0..9
letraOuDigito --> letra | digito
inteiro --> 0,1, 2, 3 ...
inteiroHexa --> 0x00, 0x01, ...0xFF..0xFFFFFF...
real --> inteiro [.] inteiro
real --> inteiro [.] inteiro exp sinal inteiro
exp
--> e | E
sinal --> + | Exercı́cio 8.1.2 (MINI-PROJETO) Especifique como autômato ou como expressões regulares e depois programe (usando o método com GOTO ou IF-WHILE) o reconhecimento de comentários em C++ que são de dois tipos: (1) dupla // até o fim de linha e (2)
múltiplas linhas com abre e fecha /* */ como exemplificado abaixo indicado abaixo.
1
2
3
void f()
{ cout << 10;
4
5
6
};
/* este eh um comentario */
// funcao vazia
/* comentario de duas linhas
esta eh a segunda
*/
// este tambem eh
Exercı́cio 8.1.3 Programe os comentários da linguagem C++, de forma que possam vir
aninhados. Use o método DRSR. É necessária uma gramática livre de contexto. Eles
podem ser de dois tipos: (1) dupla // até o fim de linha e (2) múltiplas linhas com abre e
fecha /* */ como exemplificado abaixo. Um abre comentário sem um fecha correspondente
é erro.
1
2
3
void f()
{ cout << 10;
4
5
6
};
/* este /* eh um */ comentario */
// funcao vazia
/* comentario // de duas linhas
esta eh /* a segunda */
*/
// este tambem eh
Exercı́cio 8.1.4 Programe a GA dada abaixo, com atributos herdados e sintetizados,
numa linguagem imperativa.
1
2
3
4
5
%% L-GA em DCG
b0(
M )--> {AC:=0}, b(AC,M).
b(AC, M )--> [b], {AC1:=AC+1}, b(AC1,M).
b(ACi,ACo)--> [], {ACo:=ACi}.
%% AC é um ACumulador
CAPÍTULO 8. EXERÍCIOS E PROJETOS DE PROGRAMAÇÃO
113
Exercı́cio 8.1.5 Programe a GA dada abaixo, na notação de Knuth, com atributos herdados e sintetizados, numa linguagem imperativa.
n
bs1
bs
b
b
-->
-->
-->
-->
-->
bs
b bs2
[]
0
1
{bs.N
{bs2.N
{bs.R
{b.B
{b.B
:=
:=
:=
:=
:=
0; n.R := bs.R}
2*bs1.N+b.B; bs1.R := bs2.R}
bs.N}
0}
1}
Exercı́cio 8.1.6 Programe a DCG abaixo como uma GA numa linguagem imperativa.
?-move(F,D,[esq,dir,esq,frente,frente,dir,dir,pare],[]).
F= 0+0+0+1+1+0+0+0,
D=-1+1-1+0+0+1+1+0 Yes
1
2
3
4
5
6
7
move(0,0) --> [pare].
move(D,F) --> passo(D,F).
move(D+D1,F+F1)--> passo(D,F), move(D1,F1).
passo( 1, 0) --> [dir].
passo(-1, 0) --> [esq].
passo( 0, 1) --> [frente].
passo( 0,-1) --> [traz]
8.2
Integrando Léxico e Sintático
A Figura 8.1 mostra a conexão via pipe entre um componente léxico e um componente
sintático. Na integração de componentes léxicos e sintáticos existem alguns assuntos.
Devemos trabalhar com arquivos? Como será o pipe entre os componentes? Palavra por
palavra ou fita completa? No nı́vel sintático, devemos informar os erros apontando a
linha e a coluna? Vamos comentar a solução de algumas destas questões nos exercı́cios
que seguem.
Figura 8.1: Integração entre os componentes Léxico e Sintático.
Na apresentação dos métodos, nos capı́tulos anteriores, consideramos um simples
string com a entrada do componente léxico, porém num programa maior pode ser um
arquivo de texto. Devemos saber como adaptar os métodos apresentados para trabalhar
com arquivos. Junte-se a isso existem duas abordagens para se trabalhar com arquivos:
1. trabalhar por demanda; palavra por palavra;
CAPÍTULO 8. EXERÍCIOS E PROJETOS DE PROGRAMAÇÃO
114
2. ler o arquivo todo e gerar uma fita de tokens .
Um léxico trabalha por demanda quando é codificado como uma função que retorna o
próximo token (proxToken). Isto é, o token deve ser criado para o processamento desde
o estado inicial até o estado final. Por exemplo, para um léxico de números inteiros,
cada vez que é chamada a função proxToken um valor inteiro é devolvido. Em outras
palavras, o léxico implementa a função x, xp, dos exemplos implementados em linguagens
imperativas, para uma gramática livre de contexto.
Na integração de um componente léxico com um componente sintático, se é necessário
mostrar os erros de sintaxe na posição linha e coluna, deve-se retornar estes atributos
para cada token.
Exercı́cio 8.2.1 Programe uma GA para os comentários da linguagem C++, de forma
que possam vir aninhados. Retorne para cada comentário três informações: posição que
começa (linha e coluna), posição que termina e número de caracteres que possui. Eles
pode ser de dois tipos: (1) dupla // até o fim de linha e (2) múltiplas linhas com abre
e fecha /* */ como exemplificado abaixo indicado abaixo. Um abre comentário sem um
fecha correspondente é erro.
8.3
Gramática fatorada: sem retrocesso
Exercı́cio 8.3.1 Programe a gramática abaixo numa linguagem imperativa. Assuma a
fita de entrada como um string, de caracteres: valores inteiros só entre 0 e 9.
1
2
3
4
5
6
7
8
9
10
expr --> termo,rexpr.
rexpr --> [+],expr, {write(some),nl}.
rexpr --> [-],expr, {write(subt),nl}.
rexpr --> [].
termo--> fator,rtermo.
rtermo--> [*],termo, {write(mult),nl}.
rtermo--> [/],termo, {write(divi),nl}.
rtermo--> [].
fator --> [X],{integer(X)},{write(X), write(’ enter’), nl}.
fator --> [’(’], expr, [’)’].
O efeito das ações semânticas é escrever uma seqüência de passos a serem executados
numa calculadora do tipo HP para se calcular a expressão. Esta notação para representar
expressões sem parênteses é também chamada de notação polonesa. Como segue:
?- expr([1,+,2,*,3],[]).
10 enter
20 enter
33 enter
mult
CAPÍTULO 8. EXERÍCIOS E PROJETOS DE PROGRAMAÇÃO
115
some
?- expr([1,-,2,+,4,-,3],[]).
1 enter
2 enter
4 enter
3 enter
subt
some
subt
Exercı́cio 8.3.2 PROJETO: A gramática que gera notação polonesa não é associativa à
esquerda. Reveja a solução proposta acima para parentizar uma expressão com associatividade à esquerda e utilize o método para fazer a geração do código em notação polonesa
da forma correta.
Exercı́cio 8.3.3 PROJETO: Fatore a versão da gramática que calcula o valor da expressão, com o problema da associatividade resolvido. Note que fatorar uma gramática
de atributos, implica na rescrita das equações semânticas.
Exercı́cio 8.3.4 PROJETO: Abaixo temos uma gramática para expressões booleanas.
Definimos uma ordem de precedência (maior) - ^ v -> = (menor).
Para avaliarmos uma expressão corretamente devemos também trabalhar com a associatividade à esquerda. Implemente uma DCG para parentizar expressões booleanas,
considerando à associatividade à esquerda.
1
2
3
4
5
E4
E3
E2
E1
E0
-->
-->
-->
-->
-->
8.4
t | f | Q ... | (-E0)
E4 ^ E3 | E4
E3 v E2 | E3
E2 -> E1 |E2
E1 = E0 | E1
Gramática não fatorada: método da costura
Abaixo apresentamos uma gramática (em Prolog) que traduz uma lista de dı́gitos no
valor por extenso e vice versa. Por exemplo, se perguntarmos quanto é por extenso o
valor ”123” o sistema responde ”cento e vinte e três”. E, se perguntarmos qual é o valor
para ”cento e vinte e três”, ele responde 123. Portanto, esta versão da gramática pode
ser utilizada tanto para reconhecimento como a geração; de valores ou de valores por
extenso.
ddd(C,[1,2,3],[]).
C = [cento, e, vinte, e, tres]
Yes
CAPÍTULO 8. EXERÍCIOS E PROJETOS DE PROGRAMAÇÃO
116
?- ddd([cento, e, trinta, e, um],V,[]).
V = [1, 3, 1]
Yes
Nesta gramática DCG o número e a sentença gerada são representados por listas;
numa implementação, numa linguagem imperativas podem ser representados por string
de caracteres.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
d([um])-->[1].
d([dois])-->[2].
d([tres])-->[3].
%%...
dd([dez])-->[1,0].
dd([onze])-->[1,1].
%%...
dd([vinte])-->[2,0].
dd([vinte,e|D])-->[2],d(D).
dd([trinta])-->[3,0].
dd([trinta,e|D] )-->[3],d(D).
%%...
ddd([cem])-->[1,0,0].
ddd([cento,e|DD])-->[1],dd(DD).
Note que nesta gramática se temos dois valores, 100 e 1000 só podemos decidir no
inicio da sentença se o um é lido como cem ou como mil após lermos todo o valor.
Portanto esta gramática exige olhar para frente k posições LL(k). Neste caso é mais fácil
programar uma GA com retrocesso.
Exercı́cio 8.4.1 PROJETO: Faça duas versões de um programa imperativo, usando o
método da costura: um para gerar o extenso e outro para reconhecer o extenso e gerar o
valor, conforme orientação abaixo. Faça um comparativo de tempo de execução repetindo
a entrada: 10 mil vezes.
Na versão que gera por extenso, assuma na entrada um string de dı́gitos consecutivo,
por exemplo, 9999#, 123#. Limpe os zeros à esquerda. Não é necessário controlar os
erros.
Na versão que reconhece o valor por extenso. Reconheça erros na entrada indicando
a posição. O léxico substitui o valores por extenso numa nova fita como mostra abaixo.
O valor deve ser devolvido num atributo e escrito no final.
1
2
string: nove mil e novecentos e noventa e oito#
fita: 9, 1000,-1, 900,
-1, 90,
-1,
8# (vetor de inteiros)
3
4
5
6
string: nove mil ou novecentos e noventa e oito#
1234567890123456789012345678901234567890
^---ERRO NA COLUNA (11)
CAPÍTULO 8. EXERÍCIOS E PROJETOS DE PROGRAMAÇÃO
8.4.1
117
Calcular expressões aritméticas com variáveis
A linguagem LET, já apresentada no capı́tulo 4, é uma mini linguagem interessante
para ser estudada, pois exige uma tabela de sı́mbolos com contexto para armazenar as
variáveis parciais usadas numa expressão aritmética. Ela permite calcular expressões
LET aninhadas como as que seguem:
let a=4+5, b=8+2
in a + b
VALOR=(4+5)+(8+2) = 19
let c= (let a=4+5, b=8+2
%% aqui o par^
entese é opcional
in a + b),
%% porém facilita a leitura
d=8+2
in (c+d)*c+d
VALOR=(4+5+ (8+2)+ (8+2))* (4+5+ (8+2))+ (8+2)= 561
Abaixo temos uma gramática em Prolog para estas expressões. Primeiro codificamos
dois predicados para implementar uma tabela de sı́mbolos, como uma lista de pares
par(VAR,VAL):
• lookUp/2 — retorna o valor para uma variável;
• insert/2 — insere um par(VAR,VAL) na tabela de sı́mbolos.
Como temos expressões aritméticas simples, do tipo (c+d)*c+d, a idéia é reusar uma
versão simplificada da gramática de expressões já discutida. Não trabalharemos com as
operações de soma e divisão pelo problema de associatividade à esquerda discutido no
capı́tulo 2. Agora todas as produções recebem um atributo herdado, que é a tabela de
sı́mbolos. Ela é necessária pois agora um fator pode ser uma variável: neste caso o seu
valor está registrado na tabela de sı́mbolos (para um expressão bem formada).
Na gramática LET são usadas três novas produções let, decVar e decVars. A produção let define uma expressão composta de declaração e o corpo da expressão onde as
declarações serão usadas. A produção decVar declara uma variável associada a uma expressão - no final a variável e a expressão são incluı́das na tabela de sı́mbolos. A produção
decVars declara um ou mais pares Var=Exp separados por vı́rgula.
1
2
3
4
5
6
7
8
lookUp(X,T):-member(X,T).
insert(X,Ti/To):-To=[X|Ti], write((tab:To)),nl.
isLetra(X):-member(X,[a,b,c,d,e,f,g,h,i,x,y,z]).
%%
let(Ti,V) --> [let], decVars(Ti/T1), [in], expr(T1,V).
decVars(Ti/To) --> decVar(Ti/T1), [’,’], decVars(T1/To).
decVars(Ti/To) --> decVar(Ti/To).
decVar(Ti/To) --> [L],{isLetra(L)}, [=], expr(Ti,E),
CAPÍTULO 8. EXERÍCIOS E PROJETOS DE PROGRAMAÇÃO
{insert(par(L,E),Ti/To)}.
9
10
11
12
13
14
15
16
17
18
118
%%
expr(TAB,E)-->
expr(TAB,E)-->
expr(TAB,E)-->
termo(TAB,T)-->
termo(TAB,F)-->
fator(TAB,X)-->
fator(TAB,E)-->
fator(TAB,V)-->
19
let(TAB,E).
termo(TAB,T),[+],expr(TAB,Eo),{E = (T+Eo)}.
termo(TAB,E).
fator(TAB,F),[*],termo(TAB,To),{T = (F*To)}.
fator(TAB,F).
[X],{integer(X)}.
[’(’],expr(TAB,E), [’)’].
[X],{member(X,[a,b,c,d,e,f,g,h,i,x,y,z])},
{lookUp(par(X,V),TAB), write((look:X:V)),nl}. %% vars
Os testes devem ser digitados num arquivo. Podem ser comentados. Podem existir
várias expressões no mesmo arquivo; separadas por ponto e vı́rgula. Podem existir linhas
em branco.
/* exemplos de testes:
para calculadora LET */
let a=4+5, b=8+2 // comentário de final linha
in a + b;
// VALOR=(4+5)+(8+2) = 19
let c= (let a=4+5, b=8+2
// aqui o par^
entese é opcional
in a + b),
// porém facilita a leitura
d=8+2
in (c+d)*c+d;
// VALOR=(4+5+ (8+2)+ (8+2))* (4+5+ (8+2))+ (8+2)= 561
Abaixo segue a execução dos testes da versão em Prolog. Incluı́mos dois write(s)
para depurar o programa. Aqui vemos que este programa trabalha com retrocesso: em
alguns casos ele inclui na tabela de sı́mbolos resultados que ainda não são definitivos; ao
mesmo tempo ele acessa a tabela varias vezes desnecessariamente.
?- teste(1,LET),let([],V,LET,RESTO),VX is V.
tab:[par(a,
tab:[par(b,
tab:[par(b,
tab:[par(b,
look:a:4+5
look:a:4+5
look:b:8+2
look:b:8+2
4+5)]
8+2), par(a, 4+5)]
8), par(a, 4+5)]
8+2), par(a, 4+5)]
CAPÍTULO 8. EXERÍCIOS E PROJETOS DE PROGRAMAÇÃO
119
look:b:8+2
look:b:8+2
LET = [let, a, =, 4, +, 5, (’,’), b, =|...]
V = 4+5+ (8+2)
RESTO = []
VX = 19
?- teste(2,LET),let([],V,LET,RESTO), VX is V.
tab:[par(d, 8+2), par(c, 4+5+ (8+2))]
look:c:4+5+ (8+2)
look:d:8+2
LET = [let, c, =, let, a, =, 4, +, 5|...]
V = (4+5+ (8+2)+ (8+2))* (4+5+ (8+2))+ (8+2)
RESTO = []
VX = 561
Exercı́cio 8.4.2 Implemente uma técnica de diagnóstico para erros léxicos e sintáticos
indicando a linha e coluna.
Exercı́cio 8.4.3 Implemente um diagnóstico para erros semânticos. As variáveis declaradas em vars numa expressão let vars in expr só podem ser usadas num contexto
mais interno in expr. Seguem abaixo dicas para diagnóstico de erros semânticos.
let a=b+5, b=8-2 /** em a=b+5 a variável b ainda n~
ao foi declarada **/
in let c=a+b, d=a+a+3
in (c+d)*(c+d)/2;
let a=b1+5, b=let k=2+3 in k+k
in (b+c+k);
/** em b+c+k a
/** ela é local
let a=5, b=8-2
in let a=a+1
in a+b;
/** esta express~
ao é
/** vale a declaraç~
ao
variável k já n~
ao existe **/
ao outro let
**/
válida e o aqui o a=6 **/
mais interna
**/
Modifique o predicado do lookUp para dar o diagnóstico dizendo quando ele não
encontra a variável na tabela de sı́mbolos.
Exercı́cio 8.4.4 Integre com o léxico de duas formas: a) trabalhando com uma fita toda;
b) trabalhando por demanda. Faça um comparativo de tempos de execução com um arquivo
de entrada grande: 100 linhas. Mostre uma comparação num gráfico.
Referências Bibliográficas
[1] A. Aho, R. Seti. e J. Ulmman. Compilers: Principles, Techniques, and Tools.
Addison-Wesley, Reading, MA, 1986. (ver versão traduzida)
[2] H. Ait-Kaci, Warren’s Abstract Machine: A Tutorial Reconstruction, MIT Press,
Cambridge, 1991, (also in the Web).
[3] I. Brakto, Prolog Programming for Artificial Intelligence, Second Edition, AddisonWesley Publishing Company. 1990.
[4] M. A. Casanova, F. A. C. Giorno e A. L. Furtado, Programação em Lógica e a
Linguagem Prolog Edgar Blücher Ltda, Rio de Janeiro, 1987.
[5] W. F. Clocksin e C. S. Mellish, Programming in Prolog Springer-Verlag, 4th edition,
1994.
[6] A. Comerauer, H. Hanoui, P. Roussel e R. Pasero. ”Un systeme de communication
Homme-Machine en Français”, Groupe d’Intelligence Artificielle, Université d’AixMarseille, France, 1973.
[7] M. A. Covington, D. Nute e A. Velino, Prolog Programming in Depth, Prentice Hall,
New Jersey, 1997.
[8] M. A. Covington, Natural Language Processing for Prolog Programmers, Prentice
Hall, New Jersey, 1994.
[9] P. Deransart, A. Ed-Dbali e L. Cervoni, Prolog: The Standard – Reference Manual
Springer, Berlin, 1996.
[10] R. Duncan, Programação eficaz com Microsoft macro Assembler, Rio de Janeiro :
Campus, 1993
[11] R. A. Kowalski, The predicate calculus as a programming language, International
symposium and summer school on Mathematical Foundation of Computer Science
Jablona, Poland, 1972.
[12] P.B. Menezes, Linguagens Formais e Automatos. Porto Alegre: Sagra-Luzzato Instituto de Informática UFRGS, 2000. (2 ed. Série Livros Didáticos, 3).
120
REFERÊNCIAS BIBLIOGRÁFICAS
121
[13] C. J. Hooger, Essentials of Logic Programming, Oxford University Press, Oxford,
1990.
[14] A.M.A. Price, e S.S. Toscani, Implementação de linguagens de Programação: Compiladores. Porto Alegre: Sagra-Luzzato - Instituto de Informática UFRGS, 2000. (2
ed. Série Livros Didáticos, 9).
[15] S. J. Russell e P. Norvig, Artificial Intelligence: A modern approach, Prentice Hall,
New Jersey, 1995.
[16] P. Van Roy, 1983-1993: The Wonder Years of Sequential Prolog Implementation,
Journal of Logic Programming, 1993.
[17] L. Sterling e E. Shapiro, The Art of Prolog, The MIT Press, Cambridge, 1986.
[18] D.H.D Warren, WARPLAN: a system for generate plans, Memo 76, Department of
Artificial Intelligence, University of Edinburgh, Scotland, 1974.
Índice Remissivo
at end of stream, 81
close, 80
digitos, 82
get code, 82
isDigito, 82
isLetOuDig, 82
isLetra, 82
isSeparador, 81
isSimbolo, 82
open, 80
peek code, 82
set input, 81
tokensN, 81
esquema de Banco de Dados, 71
estados (autômato), 21
fatoração de produções, 48
geração de código, 47
gramática, 4
gramática de cláusulas definidas, 2, 35
gramática livre de contexto, 2
gramática regular, 2, 21
gramática sensı́vel ao contexto, 2, 35
Irons, 2
Kleene, 2
Knuth, 2
ações semânticas, 47
álgebra relacional, 71
análise léxica, 73
árvore de derivação, 5
árvore sintática, 5
atributos herdados, 36
atributos sintetizados, 36
autômato determinı́stico, 25
automato, 73
avaliar expressões, 43
léxico (arquivos), 80
linguagem para expressões, 49
linguagens (classificação), 12
Lookahead, 82
mini-linguagem (léxico), 79
mini-linguagem (Pascal), 79
Naur, 2
notação polonesa, 48
números binários, 42
números de colunas, 83
números de linhas, 83
Backus, 2
Backus Normal Form, 2
BNF, 2
Chomsky, 2
DCG, 2, 35
Definite clause grammar, 2, 35
derivação, 4
derivações, 13
palavras reservadas, 83
parte fracionária, 41
precedência de operadores, 43
primitivas ISO, 79
produção, 4
equações semânticas, 38
equivalência de gramáticas, 29, 43
recursividade à esquerda, 43
regras reversı́veis, 49
122
ÍNDICE REMISSIVO
scanner, 82
sentenças, 13
sı́mbolo não terminal, 5
sı́mbolo terminal, 5
SQL, 71
token (automaton), 21
tokens (inteiros), 73
tokens (reais), 73
transições, 21
valor decimal, 40
valor por extenso, 49
123