Resumo:
Este trabalho consiste na implementação de um processador genérico de documentos estruturados ( XML ) que validará a boa formação dos documentos e que guardará sua informação num grove .
O trabalho é constituido por três fases. A primeira fase, a qual estamos a tratar agora consiste em construir um analisador léxico e um analisador sintático fazendo uso das ferramentas lex e yacc , para a gramática em questão para este trabalho. Sendo posteriormente armazenado o documento estruturado, se este for reconhecido.
Uma vez reconhecido o documento pode ser gerado um novo documento denominado de ESIS
Para a realização da analise léxica e sintáctica foi necessária a construção de um recinhecedor dos simbolos terminais e de um outro para reconhecer as frases válidas de uma linguagem, utilizando para o efeito o analisador léxico flex e o yacc .
No analisador léxico é onde são reconhecidos os simbolos terminais, os quais são representados por expressões regulares. As quais foram utilizadas para reconhecer o identificador de uma Tag ( ID ), para o valor dos atributos ( VALOR ), para o texto ( TEXTO ), para espaços em branco ( SPACES ) e para reconhecer os simbolos "/" e "=" foi utilizada a expressão regular SIMB .
Exemplo:
ID [_a-zA-Z][_a-zA-Z0-9\-]*
VALOR \"[^\"]+\"
SIMB [/=]
SPACES [ \t\n]
TEXTO [^\<&]+
No decorrer da implementação do analisador léxico achamos necessário a utilização de condoções de contexto, para que não surgissem conflitos. Foram utilizadas duas condições de contexto, uma delas é activada quando encontra um < e a outra é activada quando enconta um "e comercial". Dentro de cada uma destas condições de contexto são reconhecidos os simbolos terminais adequados, para não haver qualquer conflito por exemplo entra o ID e o TEXTO .
A análise sintáctica foi efectuada com a ferramenta yacc , na qual se faz o reconhecimento de cada uma das produço&etilde;s da gramática em questão .
No desenvolver da análise sintáctica foi necessário a criação de uma função chamada yyerror , a qual é invocada pelo parser yacc quando ocorre algum erro sintáctico.
Exemplo:
int yyerror(char *s)
{
flag=0;
printf("%s in line %d => ",s,yylineno);
return(1);
}
Esta função também escreve o número da linha onde ocorreu o erro sintáctico. Este processo é feito acrescentando ao analisador léxico a opção %option yylineno , escrevendo posteriormente esta opção no analisador sintáctico.
A estrutura de dados utilizada para a construção do grove foi a seguinte:
Exemplo:
typedef struct _Anodo
{
char *id_at;
char *valor;
struct _Anodo *next;
}ATRIBS;
typedef struct _Gnodo
{
char *id;
union C
{
char *txt;
struct X
{
struct _Gnodo *in;
ATRIBS *atribs;
}TAG;
}CONT;
struct _Gnodo *b;
}GROVE;
A estrutura de dados ATRIBS foi destinada ao armazenamento dos atributos das Tag's . A estrutura de dados GROVE é a principal, a qual armazena toda a informação de um documento estruturado.
Para a implementação deste grove foi necessário a criação de algumas funções auxiliares para "facilitar a vida" ao programar.
ATRIBS *newATRIB(void);
GROVE *newGROVE(void);
ATRIBS *concAttr(ATRIBS *, ATRIBS *);
GROVE *concElem(GROVE *, GROVE *);
void printAttr(ATRIBS *);
BOOL goodStr(char *);
void printTxt(GROVE *);
void geraESIS(GROVE *);
A função newATRIB cria e inicializa uma nova estrutura do tipo ATRIBS . A função newGROVE faz exactamente a mesma coisa mas para o tipo de dados GROVE . As funções concAttr e concElem foram implementadas para concatenar "atributos" e "elementos", para não perder a ordem das Tag's. As funções printAttr e printTxt foram implementadas para escrever no ecrãn os atributos de uma tag e o texto, respectivamente. A função goodStr simplesmente verifica se uma string tem caractéres que sejam visíveis no ecrãn, para determinar se essa string deve ser escrita ou não. E finalmente temos a função geraESIS que faz a travessia de um grove gerando um documento ESIS .
Código do módulo responsável pela análise léxica :
Exemplo:
%{
int lexerror=0;
%}
ID [_a-zA-Z][_a-zA-Z0-9\-]*
VALOR \"[^\"]+\"
SIMB [/=]
SPACES [ \t\n\r]+
TEXTO [^\<&]+
%option yylineno
%x tag eC
%%
& {BEGIN(eC);return('&');}
<eC>{ID} {yylval.id=(char *)strdup(yytext);return(ID);}
<eC>{SPACES} ;
<eC>; {BEGIN(0);return(';');}
\< {BEGIN(tag);return('<');}
<tag>{ID} {yylval.id=(char *)strdup(yytext);return(ID);}
<tag>{VALOR} {yylval.vl=(char *)strdup(yytext);return(VALOR);}
<tag>{SIMB} return(*yytext);
<tag>{SPACES} ;
<tag>> {BEGIN(0);return('>');}
<tag>\< lexerror=1;
<tag>. return(*yytext);
{SPACES} ;
{TEXTO} {yylval.tx=(char *)strdup(yytext);return(TXT);}
Código do módulo responsável pela análise sintáctica :
Exemplo:
%{
#include<string.h>
#include"grove.h"
int yaccerror=0;
int flag=1;
GROVE *g;
%}
%token ID VALOR TXT
%union { char *tx;
char *id;
char *vl;
GROVE *gr;
ATRIBS *at; }
%type <tx> TXT
%type <id> ID
%type <vl> VALOR
%type <gr> Doc Elem ElemList
%type <at> Attr AttrList
%start Doc
%%
Doc : '<' ID AttrList '>' ElemList '<' '/' ID '>'
{
if(!strcmp($2,$8) && !yaccerror && !lexerror)
{
g=newGROVE();
g->id=(char *)strdup($2);
g->CONT.TAG.in=newGROVE();
g->CONT.TAG.in=$5;
g->CONT.TAG.atribs=newATRIB();
g->CONT.TAG.atribs=$3;
g->b=NULL;
}
else
{
g=NULL;
flag=0;
}
};
ElemList : ElemList Elem
{ $$=concElem($1,$2); }
| { $$=NULL; };
Elem : TXT
{ $$=newGROVE();
$$->id=(char *)strdup("0-txt");
$$->CONT.txt=(char *)strdup($1);
$$->b=NULL; }
| '&' ID ';'
{ $$=newGROVE();
$$->id=(char *)strdup($2);
$$->b=NULL; }
| '<' ID AttrList '>' ElemList '<' '/' ID '>'
{ if(strcmp($2,$8)) yaccerror=1;
$$=newGROVE();
$$->id=(char *)strdup($2);
$$->CONT.TAG.in=newGROVE();
$$->CONT.TAG.in=$5;
$$->CONT.TAG.atribs=newATRIB();
$$->CONT.TAG.atribs=$3;
$$->b=NULL; }
| '<' ID AttrList '/' '>'
{ $$=newGROVE();
$$->id=(char *)strdup($2);
$$->CONT.TAG.in=NULL;
$$->CONT.TAG.atribs=newATRIB();
$$->CONT.TAG.atribs=$3;
$$->b=NULL; };
AttrList : AttrList Attr
{ $$=concAttr($1,$2); }
| { $$=NULL; };
Attr : ID '=' VALOR
{ $$=newATRIB();
$$->id_at=(char *)strdup($1);
$$->valor=(char *)strdup($3);
$$->next=NULL; };
%%
#include<stdio.h>
#include<stdlib.h>
#include<ctype.h>
#include"lex.yy.c"
int yyerror(char *s)
{
flag=0;
printf("%s in line %d => ",s,yylineno);
return(1);
}
int main(int argc, char *argv[])
{
yyparse();
if(flag)
{
if(argc==2)
if(!strcmp(argv[1],"-esis"))
{
printf("Valid Document !!!\n");
geraESIS(g);
}
else
printf("Wrong parameter !!!\n");
else
if(argc==1)
printf("Valid Document !!!\n");
}
else
printf("Invalid Document !!!\n");
return(1);
}
Código do módulo responsável pelo tratamento e armazenamento do um documento estruturado num grove :
Exemplo:
#include<stdio.h>
#include<stdlib.h>
#include<ctype.h>
#define BOOL unsigned short
#define TRUE 1
#define FALSE 0
typedef struct _Anodo
{
char *id_at;
char *valor;
struct _Anodo *next;
}ATRIBS;
typedef struct _Gnodo
{
char *id;
union C
{
char *txt;
struct X
{
struct _Gnodo *in;
ATRIBS *atribs;
}TAG;
}CONT;
struct _Gnodo *b;
}GROVE;
ATRIBS *newATRIB(void)
{
ATRIBS *a;
a=(ATRIBS *)malloc(sizeof(ATRIBS));
a->id_at=NULL;
a->valor=NULL;
a->next=NULL;
return(a);
}
GROVE *newGROVE(void)
{
GROVE *g;
g=(GROVE *)malloc(sizeof(GROVE));
g->id=NULL;
g->CONT.txt=NULL;
g->CONT.TAG.in=NULL;
g->CONT.TAG.atribs=NULL;
g->b=NULL;
return(g);
}
ATRIBS *concAttr(ATRIBS *a1, ATRIBS *a2)
{
while(a1)
{
a1->next=concAttr(a1->next,a2);
return(a1);
}
a1=a2;
return(a1);
}
GROVE *concElem(GROVE *g1, GROVE *g2)
{
while(g1)
{
g1->b=concElem(g1->b,g2);
return(g1);
}
g1=g2;
return(g1);
}
void printAttr(ATRIBS *at)
{
if(at)
{
printf("A %s %s\n",at->id_at,at->valor);
printAttr(at->next);
}
}
BOOL goodStr(char *s)
{
int i;
for(i=0;i<strlen(s);i++)
if(isprint(s[i]) && !isspace(s[i]))
return(TRUE);
return(FALSE);
}
void printTxt(GROVE *g)
{
if(g)
if(goodStr(g->CONT.txt))
printf("- %s\n",g->CONT.txt);
}
void geraESIS(GROVE *gr)
{
if(gr)
{
if(!strcmp(gr->id,"0-txt"))
{
printTxt(gr);
geraESIS(gr->b);
}
else
{
printf("( %s\n",gr->id);
printAttr(gr->CONT.TAG.atribs);
geraESIS(gr->CONT.TAG.in);
printf(") %s\n",gr->id);
geraESIS(gr->b);
}
}
}
A "famosa" makefile que nos facilita a compilação, sendo este o motivo da sua criação. Com ela não necessitamos de compilar todos os programas/módulos um de cada vez perdendo assim algum tempo e "paciência". Com simplesmente a execução do commando make a nossa makefile entra em "acção" e temos o nosso programa prontinho a correr, isso se não houver qualquer tipo de "bugsito".
Exemplo:
TARGETS = clean lex.yy.c y.tab.c grove.o ana
LIBS = -lfl
CFLAGS = -g -Wall
all:$(TARGETS)
lex.yy.c:ana.lex
flex $^
y.tab.c:ana.yacc
yacc $^
grove.o:grove.c
gcc -c $^ $(CFLAGS)
ana:y.tab.c
gcc -o $@ $^ grove.o $(LIBS)
clean:
rm -rf lex.yy.c
rm -rf y.tab.c
rm -rf grove.o
rm -rf ana
rm -rf core
Este trabalho, como acima foi referido, pode fazer uma travessia para gerar um documento ESIS , para o qual é necessário invocar o nosso "programita" com a opção "-esis", gerando assim um documento ESIS . Se esta opção não for utilizada o programa simplesmente verifica se o documento a analisar é ou não válido. Em caso de um syntax error também nos é indicada a linha onde este ocorreu.
Com este trabalho de Processamento de Linguangens I sobre gramáticas, documentos estruturados, compiladores reforçamos os nossos conhecimentos sobre algumas ferramentas novas, a nosso ver.
Foi um trabalho bastante completo no que que diz respeito à programação.
Agradecemos aos docentes da cadeira pelas aulas teóricas e teórico-práticas e pelas explicações extra-aulas que nos foram consebidas, sem as quais não seriamos capazes de efectuar o trabalho.
Bibliografia: