segunda-feira, março 14, 2011

Minhas Aventuras com "Parsing": Implementando o Earley Parser

Passada a "Semana ZX81", é hora de retomar esta série de posts, apresentando uma implementação em C do algorítimo Earley Parser.

O programa apresentado supõe entrada e saída no formato do problema "ET Phone Home". O arquivo de teste original pode ser baixado de http://www.ime.usp.br/~cef/Xmaratona/problems/io/. Entretanto, o código apresentado aqui não é uma solução adequada, por ser muito lento.

O código abaixo é uma adaptação direta do algorítimo, sem grandes otimizações. Uma lista ligada é usada para implementar os conjuntos de estados. Isto tem a vantagem de simplificar as atividades de inserir e percorrer os conjuntos, porém deixa muito ineficiente a verificação da presença de um estado no conjunto. Um levantamento rápido acusou que cerca de 40% do tempo de processamento é executando a função Presente, que faz uma busca sequencial no conjunto.

As regras para cada não terminal também são armazenadas como uma lista ligada, sem impacto significativo no desempenho.

O define DEBUG controla a listagem dos estados. Esta listagem serve tanto para depuração do código como para permitir examinar o funcionalmento do algorítimo, listando os estados que estão sendo analisados e os novos estados acrescentados por Completer, Predictor e Scanner.


// Earley Parsing
// Daniel Quadros, 2011

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define FALSE 0
#define TRUE 1

// define para mostrar mensagens de acompanhamento
#define DEBUG

// Dimensionamento (problema "ET Phone Home")

#define N_NT 27 // @A..Z
#define T_REGRA 2 // tamanho do texto da regra após "-> "
#define T_TEXTO 50 // tamanho máximo do texto a analisar

// terminais
char term [96]; // "caracteres imprimíveis" (0x21 a 0x7E)

// não terminais
char nt [N_NT]; // não terminais

// regras
typedef struct _regra
{
char dest; // NT descrito pela regra
int tam; // tamanho do texto da regra
char texto[T_REGRA]; // texto da regra
char nt [T_REGRA]; // nt[i] indica se texto[i] é NT
struct _regra *prox; // para a lista ligada de regras
} REGRA;

REGRA *tbRegra [N_NT]; // regras para os não terminais
// tbRegra[0] é a regra para a raiz

// state
typedef struct _state
{
REGRA *pRegra; // ponteiro para a regra sendo aplicada
int ponto; // posição onde estamos na regra
int inicio; // posição no texto onde começa o reconhecimento
struct _state *prox; // para lista ligada
} STATE;

STATE *S [T_TEXTO+2]; // um conjunto de estados para cada posição no texto


// Libera a memória alocada para as regras
void LimpaRegras (void)
{
REGRA *pRegra, *prox;
int i;

for (i = 0; i <= N_NT; i++)
{
prox = tbRegra[i];
while (prox != NULL)
{
pRegra = prox;
prox = pRegra->prox;
free (pRegra);
}
tbRegra[i] = NULL;
}
}


// Libera a memória alocada para os estados
void LimpaStates (void)
{
STATE *pSt, *prox;
int i;

for (i = 0; i <= (T_TEXTO+1); i++)
{
prox = S[i];
while (prox != NULL)
{
pSt = prox;
prox = pSt->prox;
free (pSt);
}
S[i] = NULL;
}
}


// Carrega as regras, que devem estar no formato do
// problema "ET Phone Home"
int CarregaRegras (void)
{
char cNT;
char szRegra [T_REGRA+6]; // "X -> texto\0"
REGRA *pRegra;
int i;


// elemento raiz
if (scanf ("%c\n", &cNT) != 1)
return FALSE;

// cria a regra @ -> raiz <vazio>
pRegra = (REGRA *) malloc (sizeof (REGRA));
pRegra->dest = '@';
pRegra->tam = 1;
pRegra->texto[0] = cNT;
pRegra->nt[0] = TRUE;
pRegra->prox = NULL;
tbRegra[0] = pRegra;

// não terminais (não inclui o @)
gets (nt);

// terminais
gets (term);

// regras
for (;;)
{
gets (szRegra);
if (szRegra [0] == '#')
break; // "# -> #" indica fim das regras
cNT = szRegra [0] - '@'; // não terminal sendo definido
pRegra = (REGRA *) malloc (sizeof (REGRA));
pRegra->dest = szRegra [0];
pRegra->tam = strlen (szRegra) - 5;
for (i = 0; i < pRegra->tam; i++)
{
pRegra->texto[i] = szRegra[i+5];
pRegra->nt[i] = strchr (term, szRegra[i+5]) == NULL;
}
pRegra->prox = tbRegra [cNT];
tbRegra [cNT] = pRegra;
}
return TRUE;
}


// Lista um estado para depuração
void DumpState (char *prefixo, STATE *pSt)
{
#ifdef DEBUG
REGRA *pReg = pSt->pRegra;
char regra [100];
int i, j, t;

t = pReg->tam + 6; // X <- xxx.xxx
regra[0] = pReg->dest;
regra[1] = ' ';
regra[2] = '<';
regra[3] = '-';
regra[4] = ' ';
for (i = 0, j = 5; j < t; i++)
{
if (i == pSt->ponto)
regra[j++] = '.';
if (i < pReg->tam)
regra[j++] = pReg->texto[i];
}
regra[t] = 0;

printf ("%s%s,%d\n", prefixo, regra, pSt->inicio);
#endif
}

// Verifica se um estado já está em um conjunto
int Presente (STATE *pSt, int i)
{
STATE *pSt2 = S[i];
while (pSt2 != NULL)
{
if ((pSt2->inicio == pSt->inicio) && (pSt2->ponto == pSt->ponto) &&
(pSt2->pRegra == pSt->pRegra))
return TRUE;
pSt2 = pSt2->prox;
}
return FALSE;
}

// Verifica se uma palavra obedece à gramática
int Analisa (char *palavra)
{
int i, n;
STATE *pSt, *pSt2, *pSt3;

n = strlen(palavra);

// coloca o estado inicial
pSt = (STATE *) malloc (sizeof (STATE));
pSt->pRegra = tbRegra[0];
pSt->ponto = 0;
pSt->inicio = 0;
pSt->prox = NULL;
S[0] = pSt;

// cria os conjuntos de estados
for (i = 0; i <= n; i++)
{
#ifdef DEBUG
printf ("> Conjunto %d\n", i);
#endif
pSt = S[i];
while (pSt != NULL)
{
DumpState (" Analisando: ", pSt);
if (pSt->ponto == pSt->pRegra->tam)
{
// Completer: reconheceu a regra
// testar se acabou
// se não, colocar no set atual as regras antigas avançando o ponto
if ((i == n) && (pSt->pRegra == tbRegra[0]) && (pSt->inicio == 0))
return TRUE;
pSt3 = S[pSt->inicio];
while (pSt3 != NULL)
{
int pto = pSt3->ponto;
char rec = pSt->pRegra->dest;

if (pSt3->pRegra->nt[pto] && (pSt3->pRegra->texto[pto] == rec))
{
pSt2 = (STATE *) malloc (sizeof (STATE));
pSt2->pRegra = pSt3->pRegra;
pSt2->ponto = pSt3->ponto + 1;
pSt2->inicio = pSt3->inicio;
if (Presente (pSt2, i))
free (pSt2);
else
{
pSt2->prox = pSt->prox;
pSt->prox = pSt2;
DumpState (" Completer: ", pSt2);
}
}
pSt3 = pSt3->prox;
}
}
else if (pSt->pRegra->nt[pSt->ponto])
{
// Predictor: tentar reconher o NT
// colocar no set atual as regras do NT
REGRA *pReg = tbRegra [pSt->pRegra->texto[pSt->ponto] - '@'];
while (pReg != NULL)
{
pSt2 = (STATE *) malloc (sizeof (STATE));
pSt2->pRegra = pReg;
pSt2->ponto = 0;
pSt2->inicio = i;
if (Presente (pSt2, i))
free (pSt2);
else
{
pSt2->prox = pSt->prox;
pSt->prox = pSt2;
DumpState (" Predictor: ", pSt2);
}
pReg = pReg->prox;
}

}
else if (pSt->pRegra->texto[pSt->ponto] == palavra[i])
{
// Scanner: avançou um terminal na regra, colocar no Set seguinte
pSt2 = (STATE *) malloc (sizeof (STATE));
pSt2->pRegra = pSt->pRegra;
pSt2->ponto = pSt->ponto + 1;
pSt2->inicio = pSt->inicio;
if (Presente (pSt2, i+1))
free (pSt2);
else
{
pSt2->prox = S[i+1];
S[i+1] = pSt2;
DumpState (" Scanner: ", pSt2);
}
}
pSt = pSt->prox;
}
if (S[i+1] == NULL)
return FALSE; // não sobrou opção
}

return FALSE;
}


// Programa Principal
// Processa entrada no formato do problema "ET Phone Home"
int main (void)
{
int iCaso = 1;
char palavra [51];

while (CarregaRegras())
{
printf ("Instancia %d\n", iCaso);
scanf ("%s\n", palavra);
while (palavra[0] != '#')
{
if (Analisa (palavra))
printf ("%s e uma palavra valida\n", palavra);
else
printf ("%s nao e uma palavra valida\n", palavra);
LimpaStates ();
scanf ("%s\n", palavra);
}
printf ("\n");
LimpaRegras ();
iCaso++;
}
return 0;
}

Nenhum comentário: