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: