Trabalho prático de Processamento de Linguagens I

Universidade do Minho 12/04/2000

Manuel Luis Fernandes Carvalho n. 24847

E-mail: si24847@ci.uminho.pt

Paulo Serafim Rodrigues Teixeira n. 24865

E-mail: si24865@ci.uminho.pt

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 .


Índice

  1. Introdução
  2. Análise léxica e sintáctica
    1. Discussaão do problema
    2. Análise léxica
    3. Análise Sintáctica
  3. Construção de um Grove
    1. Análise do Problema
    2. Funções de tratamento do grove
  4. Implementação do problema
    1. Analisador léxico (ana.lex)
    2. Analisador sintáctico (ana.yacc)
    3. O Grove (grove.c)
    4. A makefile
  5. Conclusão do problema
    1. Stage One Complete !!!

Introdução

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


Análise léxica e sintáctica

Discussaão do problema

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 .

Análise léxica

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:
Expressões regulares:

    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 .

Análise Sintáctica

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:
Função invocada pelo yacc em caso de erro:

    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.


Construção de um Grove

Análise do Problema

A estrutura de dados utilizada para a construção do grove foi a seguinte:

Exemplo:
Estrutura de dados:

    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.

Funções de tratamento do grove

Para a implementação deste grove foi necessário a criação de algumas funções auxiliares para "facilitar a vida" ao programar.

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 .


Implementação do problema

Analisador léxico (ana.lex)

Código do módulo responsável pela análise léxica :

Exemplo:
Análise léxica:

    %{
    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);}
   

Analisador sintáctico (ana.yacc)

Código do módulo responsável pela análise sintáctica :

Exemplo:
Análise sintáctica

%{
#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);
}
   

O Grove (grove.c)

Código do módulo responsável pelo tratamento e armazenamento do um documento estruturado num grove :

Exemplo:
O grove:

#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 makefile

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:
A makefile

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
   

Conclusão do problema

Stage One Complete !!!

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.


Agradecimentos:

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: