SlideShare una empresa de Scribd logo
1 de 25
Descargar para leer sin conexión
1

Apéndice 3

Introducción a la estructura y operación
de analizadores léxicos.

A3.1. Estructura de un lenguaje de programación.

Un lenguaje está basado en un vocabulario, o léxico, el que está compuesto por palabras, o
más precisamente por símbolos.

Ciertas secuencias de palabras son reconocidas como sintácticamente bien formadas o correctas.
La gramática o sintaxis o estructura del lenguaje queda descrita por una serie de reglas o
fórmulas que definen si una secuencia de símbolos es una sentencia correcta. La estructura de
las sentencias establece el significado o semántica de ésta.

Ejemplo.
<sentencia> ::= <sujeto><predicado>
<sujeto>::= árboles|arbustos
<predicado>::=grandes|pequeños

La semántica de las líneas anteriores es la siguiente:
Una sentencia está formada por un sujeto seguido de un predicado. Un sujeto es la palabra
árboles o arbustos. Un predicado consiste de una sola palabra, la cual puede ser grandes o
pequeños.
El lenguaje anterior, genera cuatro sentencias correctas.

Una sentencia bien formada puede ser derivada a partir del símbolo de partida, <sentencia> en
el caso del ejemplo, por la repetida aplicación de reglas de reemplazo o producciones (reglas
sintácticas). Se denominan símbolos no terminales, o categorías sintácticas, a las sentencias
definidas entre paréntesis de ángulo, que figuran a la derecha en las producciones; los símbolos
terminales (vocabulario) figuran a la derecha de las producciones y se representan a sí mismos.

Estas reglas para definir lenguajes se denomina formulismo de Backus-Nauer. Los paréntesis
de ángulo, y los símbolos ::= (que se lee: puede ser reemplazado por) y | (que se lee como: o
excluyente) son denominados metasímbolos.

Las producciones son libres al contexto, si en éstas figura a la izquierda un solo símbolo no
terminal S, que puede ser reemplazado en función de símbolos terminales s, no importando el
contexto en el que ocurra S.



Profesor Leopoldo Silva Bijit               20-01-2010
2                                                             Estructuras de Datos y Algoritmos
La producción: ASB::=AsB define que S puede ser reemplazado por s, siempre que ocurra
entre A y B; por lo cual es sensible al contexto.

El uso de la recursión al definir producciones, permite generar un infinito número de sentencias
a partir de un número finito de producciones.
S::=aA
A::=b|cA

La categoría A, está definida en términos de sí misma. En el ejemplo, los símbolos terminales se
representan con minúsculas, los no terminales con mayúsculas.
A partir de S, se generan: ab, acb, accb, acccb, …..

A3.2. Analizador léxico. (parser)

El diseño de un reconocedor de sentencias correctas está basado en encontrar algoritmos que
sean de complejidad n, donde n es el largo de la sentencia a analizar. Es decir en cada paso del
algoritmo se depende solamente del estado actual y del siguiente símbolo; y no es necesario
volver atrás. Obviamente la estructura del lenguaje debe permitir esta forma de análisis.

El método jerárquico o top-down, de análisis de sentencias (parsing) consiste en reconstruir el
árbol de derivación desde el símbolo de partida hasta la sentencia final.

Ejemplo: Se da la sentencia: árboles grandes, y se desea determinar si pertenece al lenguaje
definido en un ejemplo anterior.
Se parte del símbolo de partida, <sentencia> y se lee el primer símbolo del texto que se desea
analizar: árboles.

Se reemplaza <sentencia> por <sujeto><predicado>, se ve si es posible reemplazar <sujeto>; se
verifica que puede ser reemplazado por árboles. En este momento, puede avanzarse al siguiente
símbolo de la secuencia de entrada, que en el caso del ejemplo es grandes. Al mismo tiempo se
avanza al siguiente de los símbolos no terminales. Ahora la tarea restante es verificar si
<predicado> puede generar el símbolo grandes. Como esto es así, se avanza en la secuencia de
entrada, y se observa que no quedan más símbolos. Con lo cual el análisis termina reconociendo
cómo válida la construcción.

Cada paso del análisis está basado solamente en el siguiente símbolo de la secuencia de
símbolos no terminales que se está analizando.

Puede ilustrarse el algoritmo mediante la siguiente tabla. La columna a la izquierda representa la
tarea de reconocimiento pendiente y la de la derecha la secuencia de símbolos terminales que
aún no se leen. Se desea validar accb como perteneciente a S.

S::=aA
A::=b|cA




Profesor Leopoldo Silva Bijit                20-01-2010
Introducción a analizadores léxicos                                                             3

                 S    accb       Inicio. Se lee a
                 aA   accb       Se reemplaza S. Se reconoce a.
                  A    ccb       Se acepta a. Se lee c
                  cA   ccb       Se reemplaza A. Se reconoce c.
                   A    cb       Se acepta c. Se lee c.
                   cA   cb       Se reemplaza A. Se reconoce c.
                    A    b       Se acepta c. Se lee b.
                    b    b       Se reemplaza A. Se reconoce b.
                    -    -       Se acepta la frase como correcta.

                              Figura A3.1. Análisis sin volver atrás.

No es necesario volver atrás, y el análisis está basado en la lectura de un símbolo terminal por
adelantado.

Para que esto sea posible, los símbolos iniciales de símbolos no terminales alternativos que
figuran a la derecha en las producciones, deben ser diferentes.

El siguiente ejemplo ilustra reglas que no cumplen el principio anterior. Ya que A y B (símbolos
no terminales alternativos en S), tienen iguales símbolos iniciales, ambos son x.
S::=A|B
A::=xA|y
B::=xB|z

Si se desea analizar la secuencia xxxz, se tendrá la dificultad que no es posible discernir (sólo
leyendo el primer símbolo por adelantado) si S debe ser reemplazado por A o por B. Si se
eligiera al azar, reemplazar S por A, luego de unos pasos el análisis falla, y se debería volver
atrás, e intentar reemplazar por B, en lugar de A, y volver a intentar.
Las reglas:
S::=C|xS
C::=y|z

Son equivalentes a las anteriores y cumplen el principio anterior.

En variadas construcciones de los lenguajes se acepta símbolos opcionales. Es decir la
alternativa entre un símbolo terminal y una secuencia nula de símbolos. Ejemplo de esto es la
asignación: x= +a; el símbolo + es opcional.

Sea nula la secuencia nula.
Entonces las reglas:
S::=Ax
A::=x|nula

Si se desea reconocer x, si luego de reemplazar S por Ax, se intenta reemplazar A por x el
análisis falla, se logra xx en la derivación. Con lo cual puede reconocerse el primer x, y luego al


Profesor Leopoldo Silva Bijit                20-01-2010
4                                                             Estructuras de Datos y Algoritmos
intentar leer el próximo, como no quedan símbolos no terminales que leer y queda pendiente un
x que derivar, se concluye que el análisis falló. Lo que se debió realizar era reemplazar A por
nula.
Para evitar la vuelta atrás en el reconocimiento, se impone una regla adicional para las
producciones que generen la secuencia nula:

Para una secuencia A que genera la secuencia nula, los símbolos iniciales que genera A deben
se disjuntos con los símbolos que siguen a cualquier secuencia generada por A.

En el ejemplo anterior, S dice que la sentencia A tiene a x como símbolo siguiente. Y la
producción que define A, indica que el primer símbolo que puede generar A es también x.
Como los iniciales generados por A son iguales a los siguientes a A, se viola la regla anterior.

La repetición de construcciones, que también es muy frecuente en los lenguajes, suele definirse
empleando recursión.
Por ejemplo la repetición de una o más veces del elemento B, puede anotarse:
A::= B|AB

Pero el primero de B y el primero de AB no es el vacío, y no cumple la primera de las reglas.
Si se cambia la definición de A por:
A::=nula|AB
A genera: nula, B, BB, BBB, … y se tendrá que el primero de A y el siguiente a A serán B,
violando la segunda regla.

Lo cual permite visualizar que no puede emplearse recursión por la izquierda.

La recursión por la derecha, cumple las reglas anteriores:
A::=nula|BA
Esta última producción también genera la repetición de cero, una o más veces del elemento B.
La frecuencia de construcciones repetitivas que generen la secuencia nula lleva a definir los
siguiente metasímbolos:
A::={B}
Que genera: nula, B, BB, BBB, …

Esta definición sólo simplifica la notación, pero aún es preciso revisar que se cumpla la segunda
regla, para emplear algoritmos basados en leer un símbolo por adelantado y sin volver atrás.

A3.3. Reglas de análisis.

Debido a lo abstracto del formalismo de Backus-Nauer se ha desarrollado una representación
gráfica de las reglas. En los grafos sintéticos se emplean los siguientes símbolos:




Profesor Leopoldo Silva Bijit               20-01-2010
Introducción a analizadores léxicos                                                            5

Símbolo terminal.

                                               x


                                 Figura A3.2. Símbolo terminal.

Corresponde a un reconocimiento del símbolo terminal x, en la producción de la que forma
parte, y al avanzar en la lectura del siguiente símbolo en la secuencia de entrada.

Es importante asociar estos grafos a elementos de un lenguaje de programación que
implementará el reconocedor sintáctico basado en diagramas.

Para el elemento terminal, puede traducirse:
if (ch== „x‟) ch=lee(stream); else error(n);

Donde stream es el descriptor del archivo que contiene el texto que será analizado. La función
lee, trae el siguiente carácter. En este nivel los caracteres individuales del texto se consideran
símbolos terminales. La función de error, debería generar un mensaje asociado a la detección de
una sentencia mal formada.

Símbolo no terminal.


                                                   B


                                Figura A3.3. Símbolo no terminal.

Cuando aparece este diagrama en una producción, corresponde a la activación de un
reconocedor del símbolo no terminal B.

En el reconocedor, se activa una invocación al procedimiento reconocedor de B.
B( );

Alternativa.

La producción: A::=B1|B2|…|Bn
Se representa:




Profesor Leopoldo Silva Bijit                  20-01-2010
6                                                                Estructuras de Datos y Algoritmos


                                                  B1
                                         A
                                                  B2

                                                  Bn


                                     Figura A3.4. Alternativa.

En el reconocedor, puede implementarse, mediante la sentencia switch.

         switch (ch){
           case L1: B1(); break;
           case L2: B2(); break;
           ….
           case Ln: Bn(); break;
         }

Donde los Li serían los conjuntos de los símbolos iniciales de los Bi.

Es preferible describir la alternativa, explicitando el primer símbolo:

                                             b1        B1

                                     A
                                             b2        B2


                                             bn        Bn

                     Figura A3.5. Alternativa, con primer símbolo explícito.

         switch (ch){
           case „b1‟: {ch=lee(stream); B1(); break;}
           case „b2‟: {ch=lee(stream); B2(); break;}
           ….
           case „bn‟: {ch=lee(stream); Bn(); break;}
         }

Concatenación.

La producción: A::=B1B2…Bn
Se representa:


Profesor Leopoldo Silva Bijit                 20-01-2010
Introducción a analizadores léxicos                                                   7


                                A
                                      B1        B2            Bn

                                    Figura A3.6. Concatenación.

En el reconocedor: {B1( ); B2( );…Bn( );}

Repetición.

La producción: A::={B}

Se representa:

                                     A

                                                 B



                                      Figura A3.7. Repetición.

En el reconocedor, se implementa:
while( esta_en(L, ch) ) B( );

Donde la función esta_en retorna verdadero si ch pertenece al conjunto L de los primeros
caracteres generados por B.

Es preferible, representar, el forma explícita el primer carácter:

                                           A

                                                      B        b


                     Figura A3.8. Repetición, con primer símbolo explícito.

De este modo el reconocedor se implementa:
while( ch==‟b‟) {ch=lee(stream); B( );}

La repetición, de a lo menos una vez, puede implementarse con una sentencia while.

Cada uno de los bloques anteriores puede ser reemplazado por alguna de las construcciones
anteriores. Por ejemplo: la repetición puede ser una serie de acciones concatenadas.




Profesor Leopoldo Silva Bijit                  20-01-2010
8                                                                  Estructuras de Datos y Algoritmos
Resumen.

Para una gramática dada, pueden construirse grafos sintácticos a partir de las producciones
descritas en BNF, y viceversa.
Y de éstas derivar el código del reconocedor.

Los grafos deben cumplir las siguientes dos reglas, para que se puedan recorrer leyendo un
símbolo por adelantado y sin volver atrás.

        Los primeros símbolos en las alternativas deben ser diferentes. De tal forma que la
        bifurcación solo pueda escogerse observando el siguiente símbolo de esa rama.
        Si un grafo reconocedor de una sentencia A, puede generar la secuencia nula, debe
        rotularse con todos los símbolos que puedan seguir a A. Ya que ingresar al lazo puede
        afectar el reconocimiento de lo que viene a continuación.

Una vez definido el lenguaje a reconocer, mediante sus grafos, debe verificarse el cumplimiento
de las dos reglas anteriores. Un sistema de grafos que cumplan las reglas anteriores se denomina
determinista y puede ser recorrido sin volver atrás y solo leyendo un símbolo por adelantado.

Esta restricción no es una limitante en los casos prácticos.

Ejemplo A3.1. Reconocedor simple.

Generar reconocedor para sentencias que cumplan las siguientes producciones.
A::=x|(B)
B::=AC
C::={+A}

Algunas sentencias válidas, de este lenguaje, son:
 x, (x), (x+x), ((x)), (x+x+x+x+x+x+x+x+x),….

Pueden plantearse los grafos sintácticos para cada una de las producciones. Posteriormente, es
posible reducir los grafos, mediante substituciones. Luego de esto se obtiene:
                           A
                                   (      A                              )

                                                      A        +


                                                      x


                                Figura A3.9. Grafo del reconocedor.

El grafo permite revisar el cumplimiento de las dos reglas.
La bifurcación tiene intersección vacía de los primeros elementos de cada rama:
{ „(„ } { „x‟ } =


Profesor Leopoldo Silva Bijit                 20-01-2010
Introducción a analizadores léxicos                                                              9
La producción que genera la secuencia nula tiene intersección vacía del primer elemento de la
repetición y del símbolo que sigue a esa repetición:
{ „+„ } { „)‟ } =

El siguiente programa implementa un reconocedor, para sentencias que cumplan la sintaxis
descrita por el grafo. Lo más importante es notar que el código para el reconocedor de
sentencias A, puede ser escrito a partir del diagrama anterior. Cada conjunto de reglas da origen
a un programa determinado. Se han agregado las funciones que abren y leen el archivo con el
texto que será analizado, para ilustrar los detalles del entorno. Se da término a las sentencias del
archivo con un asterisco, en la primera posición de una línea. Cada vez que termina el análisis
de una sentencia avisa si la encontró correcta.

Se emplea una variable global ch, para disminuir el número de argumentos. Se destaca que debe
leerse un símbolo por adelantado, antes de invocar al reconocedor.
Si el descriptor del archivo se deja como variable global, pueden disminuirse aún más los
argumentos de las funciones, simplificándolas.

#include <stdio.h>

void error(int e)
{ printf("Error %dn", e);}

char lee(FILE *stream)
{ return( fgetc(stream)); }

char ch='0';

void A(FILE *stream)
{
  if (ch=='x') ch=lee(stream);
  else
    if (ch=='(' )
      { ch=lee(stream); A(stream);
        while(ch=='+') {ch=lee(stream); A(stream);}
        if ( ch==')' ) ch=lee(stream); else error(1);
      }
    else
      error(2);
}

/* Analiza archivo de texto */
int parser(void)
{
   FILE *stream;
   if ((stream = fopen("inparser.txt", "r")) == NULL) {
      fprintf(stderr, "No pudo abrir archivo de entrada.n");
   return 1;

Profesor Leopoldo Silva Bijit                  20-01-2010
10                                                               Estructuras de Datos y Algoritmos
     }

     /* lee hasta encontrar el final del stream */
     while(!feof(stream))
     {
       ch=lee(stream); if(ch=='*') break; //lee ch por adelantado.
       A(stream);
       printf("%sn","ok");
     }
     printf("%sn","fin");

     fclose(stream);
     return 0;
}

int main(void)
{
   parser();
   return 0;
}

Ejemplo A3.2. Parser BNF.

Se desea reconocer sentencias descritas por la siguiente gramática:

producción ::= <símbolo no terminal> „=‟ <expresión> „.‟
expresión ::= <término> {„,‟ <termino>}
término ::= <factor> { <factor> }
factor ::= <símbolo terminal> | <símbolo no terminal> | „( „ <expresión> „)‟
símbolo no terminal ::= Letra mayúscula
símbolo terminal ::= Letra minúscula

Los símbolos terminales requeridos por las reglas se han colocado entre comillas simple. Nótese
que cada producción termina en el carácter punto.
La serie de producciones se termina cuando se encuentra un asterisco:
<texto de programa> ::= {producción} „*‟

El parser genera algunos comentarios de error, hacia la salida estándar, indicando la línea y la
posición del carácter que no cumple las reglas.

#include <stdio.h>
#include <stdlib.h> //malloc
#include <string.h>
#include <ctype.h>

void expresion(void); //prototipo. Factor requiere expresión.


Profesor Leopoldo Silva Bijit                  20-01-2010
Introducción a analizadores léxicos                                              11
char simbolo='0';
int nl=1; //contador de líneas
int nc=0; //contador de caracteres en la línea.
FILE *stream;

void error(int tipo)
{
 putchar('n');printf("(%d,%d): ",nl, nc+1);
 switch (tipo)
 {
   case 1: printf("%sn", "Esperaba símbolo no terminal");break;
   case 2: printf("%sn", "Esperaba signo igual"); break;
   case 3: printf("%sn", "Esperaba cierre paréntesis"); break;
   case 4: printf("%sn", "Esperaba abre paréntesis"); break;
   case 5: printf("%sn", "Esperaba punto"); break;
 }
}

void getch(void)
{
  if(!feof(stream))
  { simbolo = fgetc(stream); nc++;
    if(símbolo == 'n') {nl++; nc=0;}
    putchar(simbolo); //eco en salida estándard
  }
}

void getsimbolo(void)
{
  getch();
  while(isspace(simbolo)) getch(); //descarta blancos
}

//factor ::= <símbolo terminal> | <símbolo no terminal> | „( „ <expresión> „)‟
void factor(void)
{
 if (isalpha(simbolo) ) getsimbolo();
 else
    if (simbolo == '(' )
         { getsimbolo(); expresion();
           if(símbolo == ')' ) getsimbolo(); else error(3);
         }
    else error(4);
}




Profesor Leopoldo Silva Bijit                 20-01-2010
12                                                               Estructuras de Datos y Algoritmos
// término ::= <factor> { <factor> }
void termino(void)
{
  factor();
  while( (isalpha(simbolo)) || (símbolo == '(' ) ) factor();
}
Notar que la repetición de factor es precedida por la revisión de los primeros caracteres de
factor: es decir que sea un símbolo terminal o no terminal o el comienzo de una expresión, que
debe comenzar por paréntesis abierto.

//expresión ::= <término> {„,‟ <termino>}
void expresion(void)
{
  termino();
  while(símbolo == ',') {getsimbolo(); termino();}
}

// producción ::= <símbolo no terminal> „=‟ <expresión> „.‟
void produccion()
{
     if(isupper(simbolo)) getsimbolo(); else error(1);
     if (símbolo == '=') getsimbolo(); else error(2);
     expresion();
     if (simbolo != '.') error(5);
}

/* Lectura de archivo de texto */
int bnfparser(void)
{
    /* Abre stream para lectura, en modo texto. */
    if ((stream = fopen("bnfparser.txt", "r")) == NULL) {
       fprintf(stderr, "No pudo abrir archivo de entrada.n");
    return 1;
   }

     /* lee hasta encontrar el final del stream */
     while(!feof(stream))
     {
       getsimbolo(); if(simbolo=='*') break;
       produccion();
     }
     printf("%s numero de lineas =%dn","fin de archivo", nl);

     fclose(stream); /* close stream */
     return 0;
}


Profesor Leopoldo Silva Bijit                 20-01-2010
Introducción a analizadores léxicos                                                           13
int main(void)
{
   bnfparser();

    return 0;
}

Para el siguiente archivo de entrada:

     A = C.
B=x,A.
B=x,A,B,C-
C=x(B,D.
D=(A).
*

Se genera la siguiente salida:
       A = C.
B=x,A.
B=x,A,B,C-
(3,10): Esperaba punto

C=x(B,D.
(4,8): Esperaba cierre paréntesis

D=(A).
*fin de archivo número de líneas =5

Ejemplo A3.3. Reconocedor de identificador.

Un identificador es una secuencia de caracteres, donde el primero debe ser letra, y los que
siguen letras o números.

Un identificador puede aparecer entre espacios, tabs, retornos; o estar entre caracteres no
alfanuméricos. Si ab y cd son identificadores, las siguientes líneas muestran posibles instancias
de éstos:
                           ab
                                (ab + cd)
                            ab= cd;
                                      ab     = cd+5;

Una alternativa al diseño de reconocedores es el diseño basado en diagramas de estados. Se
ilustra un ejemplo, basado en análisis de líneas. Más adelante en A3.4 se esbozan
procedimientos para leer archivos de texto por líneas.

El diagrama de estados de la Figura A3.10, muestra que deben descartarse los espacios (se
simboliza por el círculo 0), y comenzar a almacenar el identificador, cuando se encuentra una

Profesor Leopoldo Silva Bijit               20-01-2010
14                                                              Estructuras de Datos y Algoritmos
letra; luego se siguen almacenado los caracteres del identificador (círculo 1) hasta que llegue un
carácter no alfanumérico, en que se vuelve a esperar identificadores.

Si lo único que se desea es extraer los identificadores, si no llega una letra cuando se espera una,
puede descartársela y continuar el análisis.

                                               Si es letra
               Si es espacio
                                                                    Si es alfanumérico


                                   0                            1

           No es letra


                                        Si no es alfanumérico

                      Figura A3.10. Estados de reconocedor de identificador.

Asumiendo que se tiene una línea almacenada en buffer, la siguiente función forma en el string
id, el identificador.

La estructura del código está basada en el diagrama anterior, y en las funciones cuyos prototipos
se encuentran en ctype.h

#define LARGOLINEA 80
#define Esperando_letra 0
#define Almacenando_id 1

getword(char *buffer, int nl)
{
   char id[LARGOLINEA];
   int i, j, estado;
   for(i=0, estado=0, j=0; i<strlen(buffer); i++)
        {
           switch (estado){
             case Esperando_letra:
              if (isspace(buffer[i]) ) continue;
              else
                if (isalpha(buffer[i]))
                   { estado=Almacenando_id;
                     id[j]=buffer[i]; j++;
                   }
                else ; //No es letra. Descarta char.


Profesor Leopoldo Silva Bijit                   20-01-2010
Introducción a analizadores léxicos                                                            15
               break;
              case Almacenando_id:
               if (isalnum(buffer[i]))
                  {id[j]=buffer[i]; j++;} //forma id
               else
                  if(!isalnum(buffer[i]))
                     { estado = Esperando_letra;
                       id[j]='0'; //termina string
                       j=0; //reset posición
                       //printf("%s %dn", id, nl); //muestra los identificadores y la línea

                    // Aquí debería hacerse algo con el identificador
                    root=insert(id, root, nl); //Ejemplo: lo inserta en árbol
                    }
               break;
          }
      }
}

Ejemplo A3.4. Reconocedor de una definición.

El siguiente ejemplo es una elaboración del anterior, y su código se realiza apoyándose en el
diagrama de estados de la Figura A3.11.

Se desea analizar un texto de programa y reconocer el identificador y su definición que figuran
en una línea que comienza con #define.

En el siguiente diagrama de estados, se pasa al estado 3, si se reconoce el identificador define,
luego del símbolo #. A la salida del estado 4, se tiene el identificador para el cual se está
definiendo una equivalencia. A la salida del estado 6, se tiene el identificador con el valor.
Luego del estado 6 debe regresar al estado 0.

Se emplean las definiciones:
#define LARGOID        20 //máximo largo identificadores
#define EsperaID        0 //estados
#define GetId           1
#define BuscaDefine 2
#define EsperaDef       3
#define GetDef          4
#define EsperaEquiv 5
#define GetEquiv        6




Profesor Leopoldo Silva Bijit                     20-01-2010
16                                                                       Estructuras de Datos y Algoritmos


                                              alfanum
                                                                 6                               isspace
                                                                                      5
                                 !alfanum
                                                                          alfanum
                                                                                          !alfanum

      es alfanumérico                       id ==”define”
                                                                     3                4
                                  2
                                                                          alfanum

                          Es #        id != “define”        Si es espacio           alfanum

                                            Si es letra                     Si es alfanumérico
         Si es espacio

                                 0                               1

     No es letra ni #
                                      Si no es alfanumérico


                         Figura A3.11. Estados de reconocedor de definiciones.

La función puede escribirse, basándose en el diagrama:
FILE *streami,*streamo;
char ch;
#define p() putc(ch,streamo);
#define g() ch=getc(streami)
void parser(void)
{
   char word[LARGOID];
   char equiv[LARGOID];
   int j, state;
   pcelda p;
   for(state=0, j=0;!feof(streami);g())
        {
          switch (state){
            case EsperaID:
             if (isspace(ch) ) { p(); continue;}
             else if (isalpha(ch)) {state=GetId; word[j++]=ch;}
             else if(ch=='#'){state=BuscaDefine; j=0;}
             else p(); //se traga hasta final de línea
             break;
            case GetId:
             if (isalnum(ch)) {word[j++]=ch;}
             else
               { if(!isalnum(ch))

Profesor Leopoldo Silva Bijit                    20-01-2010
Introducción a analizadores léxicos                                                     17
                 {state=EsperaID; word[j]='0'; j=0;
                  //printf("%sn",word);
                  if( (p=buscar(word))!=NULL ) //busca identificador
                    {
                      //printf("%s -> %sn", p->definicion, p->equivalencia);
                      fprintf(streamo, p->equivalencia); //lo reeemplaza
                    }
                  else fprintf(streamo,word);
                 }
              p();
            }
          break;
         case BuscaDefine:
          if (isalnum(ch)) {word[j++]=ch;}
          else
            if(!isalnum(ch))
             {word[j]='0';j=0;
               if (strcmp(word,"define")==0)
                   { state=EsperaDef;
                     //printf("pre=%sn",word);
                   }
               else {state=EsperaID; putc('#', streamo); fprintf(streamo,word);p();}
             }
          break;
         case EsperaDef:
          if (isspace(ch) ) continue;
          else if (isalpha(ch)) {state=GetDef; word[j++]=ch;}
          break;
         case GetDef:
          if (isalnum(ch)) {word[j++]=ch;}
          else if(!isalnum(ch))
                 {state=EsperaEquiv; word[j]='0'; j=0;
                  //printf("Definición =%sn",word);
                 }
          break;
         case EsperaEquiv:
          if (isspace(ch) ) continue;
          else if (isgraph(ch)) {state=GetEquiv; equiv[j++]=ch;}
          break;
         case GetEquiv:
          if (isgraph(ch)) {equiv[j++]=ch;}
          else if(!isgraph(ch))
                { state=EsperaID; equiv[j]='0';j=0;
                 //printf("insertar valor equivalente en tabla=%sn", equiv);
                 // Aquí debería insertar la palabra word y su equivalencia.
                  if( (p=buscar(word))!=NULL ) descarte(word); //permite redefinición
                  inserte(word, equiv);

Profesor Leopoldo Silva Bijit                20-01-2010
18                                                               Estructuras de Datos y Algoritmos
                    }
                break;
            }
       }
}

El siguiente segmento, abre los archivos de entrada y salida.
int procesa_archivos(void)
{
 /* Abre stream para lectura, en modo texto. */
    if ((streami = fopen("input6.c", "r")) == NULL) {
       fprintf(stderr, "No pudo abrir archivo de entrada.n");
    return 1;
   }
/* Abre stream para escritura, en modo texto. */
    if ((streamo = fopen("output6.c", "w")) == NULL) {
       fprintf(stderr, "No pudo abrir archivo de salida.n");
    return 1;
   }
/* lee hasta encontrar el final del stream */
   while(!feof(streami))
   {
       g(); //lee uno por adelantado
       if(!feof(streami))
         {
          parser();
          //putchar(ch);
         }
       else break;
   }
   fclose(streami); /* close stream */
   fclose(streamo);
   return 0;
}

int main(void)
{ makenull();
   procesa_archivos();

return 0;
}

A3.4. Manipulación de archivos en C.

En la etapa de prueba de los algoritmos es necesario ingresar datos. Si éstos son numerosos es
recomendable efectuar esta operación leyendo los datos desde un archivo.


Profesor Leopoldo Silva Bijit                20-01-2010
Introducción a analizadores léxicos                                                               19
También es preciso poder extraer resultados, para posterior análisis, escribiéndolos en un
archivo.

Escritura de archivos de texto, con estructura de líneas.

Consideremos primero la escritura de archivos de texto con formato.
Esto puede lograrse con un editor, siguiendo ciertas convenciones para separar los elementos de
datos. Es recomendable ingresar por líneas, y en cada línea separar los ítems mediante espacios
o tabs.

Es importante saber el número y tipos de datos de los ítems de cada línea, ya que debe
confeccionarse un string de formato con dicha estructura.

En caso de escritura de un archivo, mediante un programa, también debe tenerse en cuenta la
estructura de la línea.

Veamos un ejemplo.
Se tiene un entero y un carácter por línea, que configuran un dato con la siguiente estructura:

struct mystruct
{
  int i;
  char ch;
};

La confección con un editor, podría generar el siguiente texto, donde los espacios previos al
número pueden ser reemplazados por varios espacios o tabs. La separación entre el número y el
carácter debe ser un espacio o un tab. Luego pueden existir varios espacios o tabs, seguidos de
un retorno.
11 a
 22      b
     333 c
 4d
 5e

Escritura de archivo, desde un programa.

La manipulación de archivos, requiere la invocación a funciones de <stdio.h>, para abrir, cerrar,
leer o escribir en el archivo.

El siguiente segmento abre para escritura el archivo testwr.txt, que debe estar ubicado en el
mismo directorio que el programa ejecutable; en caso de otra ubicación, es preciso preceder el
nombre con el sendero de la ruta.
El modo “w” establece modo escritura, es decir sobreescribe si el archivo existe, y lo crea en
caso contrario.

La variable que permite manipular un archivo es de tipo FILE, y se la define según:

Profesor Leopoldo Silva Bijit                20-01-2010
20                                                              Estructuras de Datos y Algoritmos
FILE *stream;

Se suele proteger la apertura, en caso de falla, mediante:

if ((stream = fopen("testwr.txt", "w")) == NULL)
{
          fprintf(stderr, "No puede abrir archivo de salida.n");
          return 1;
}

El stream o flujo de salida stderr suele ser la pantalla.

La siguiente instrucción escribe en el stream 4 caracteres por línea, más el terminador de línea,
eol; que suele ser uno o dos caracteres. La estructura de la línea: dd<sp>c<eol>.

fprintf(stream, "%2d %cn", s.i, s.ch);

Una vez completada la escritura de todas las líneas, se cierra el archivo, mediante:
fclose(stream);

Lectura de archivos de texto con estructura de líneas.

Se incluye un programa para leer el archivo, ya sea generado con un editor o por un programa,
mediante la función fscanf. Se ha usado la función feof, para determinar si se llegó al final del
archivo. La acción que se realiza con los datos es simplemente desplegar en la pantalla, los
datos formateados.

/* Ejemplo con streams. Lectura de archivo de texto formateado */

#include <stdio.h>
//El archivo testwr.txt debe estar estructurado en líneas.
//Cada línea debe estar formateada según: <sep>entero<sep>char<eol>
//Donde <sep> pueden ser espacios o tabs.
//El entero debe ser de dos dígitos, si en el string de formato del fscanf figura como %2d.

FILE *stream;

int main(void)
{       int jj; char cc;

        /* Abre stream para lectura, en modo texto. */
        if ((stream = fopen("testwr.txt", "r")) == NULL){
           fprintf(stderr, "No pudo abrir archivo de entrada.n");
           return 1;
        }

        /* lee hasta encontrar el final del stream */

Profesor Leopoldo Silva Bijit                  20-01-2010
Introducción a analizadores léxicos                                                           21
        for(;;)
        {
          fscanf(stream, "%d %c", &jj, &cc); //lee variables según su tipo y estructura de línea.
          if(feof(stream)) break;
          printf("%d %cn", jj, cc); //sólo muestra las líneas del archivo
        }

        fclose(stream); /* close stream */
        return 0;
}

Si se intenta leer más allá del fin de archivo la función feof, retorna verdadero.

Llenar un arreglo a partir de un archivo.

El siguiente ejemplo llena un arreglo de estructuras.
/* Ejemplo con streams. Con datos de archivo se escribe un arreglo */

#include <stdio.h>
//El archivo testwr.txt debe estar estructurado en lineas.
//Cada linea debe estar formateada según: <sep>entero<sep>char<eol>
//Donde <sep> pueden ser espacios o tabs.
//El entero debe ser de dos dígitos, si en el string de formato del fscanf figura como %2d.

struct mystruct
{
  int i;
  char ch;
};
#define ITEMS 20
struct mystruct arr[ITEMS];
FILE *stream;

int main(void)
{
        int i, jj; char cc;
        /* Abre stream para lectura, en modo texto. */
        if ((stream = fopen("testwr.txt", "r")) == NULL)            {
                   fprintf(stderr, "No pudo abrir archivo de entrada.n");
                   return 1;
        }
        for(i=0; i< ITEMS; i++)
        {
          fscanf(stream, "%d %c", &jj, &cc); //lee variables según tipo.
          if(feof(stream)) break;
          arr[i].i=jj; arr[i].ch=cc; //llena items del arreglo
        }

Profesor Leopoldo Silva Bijit                 20-01-2010
22                                                                 Estructuras de Datos y Algoritmos

        fclose(stream); /* close stream */

        for(i=0;i< ITEMS;i++)
        {
          printf("%d %cn", arr[i].i, arr[i].ch);// muestra el arreglo
        }
        return 0;
}

Escritura y lectura de archivos binarios.

El siguiente ejemplo, ilustra la escritura y lectura de archivos binarios, no de texto. Se emplean
ahora las funciones: fwrite, fseek y fread.

* Ejemplo con streams. Escritura y luego lectura de archivo binario */

#include <stdio.h>

struct mystruct
{
  int i;
  char ch;
};

int main(void)
{
     FILE *stream;
        struct mystruct s;
        int j;
        /* sobreescribe y lo abre para escritura o lectura en modo binario */
        if ((stream = fopen("TEST.bin", "w+b")) == NULL)          {
       fprintf(stderr, "No se puede crear archivo de salida.n");
       return 1;
        }

        for(j=0;j<20;j++)
        {
          s.i = j;
          s.ch = 'A'+j;
          fwrite(&s, sizeof(s), 1, stream); /* write struct s to file */
        }

        /* seek to the beginning of the file */
        fseek(stream, SEEK_SET, 0);

        /* lee y despliega los datos */

Profesor Leopoldo Silva Bijit                   20-01-2010
Introducción a analizadores léxicos                                                        23
          for(j=0; j<20;j++)
           {
             fread(&s, sizeof(s), 1, stream);
             printf("%d %cn", s.i, s.ch);
           }

    fclose(stream); /* close file */
    return 0;
}

El archivo TEST.bin, no puede ser visualizado con un editor de texto. Para su interpretación
debe usarse un editor binario.

Compilación y ejecución en ambiente UNIX.

Para programas sencillos, como los ilustrados, puede generarse el ejecutable en ambiente UNIX,
mediante el comando: make <nombre de archivo c, sin extensión>

Esto crea un ejecutable de igual nombre al programa en C.
Para su ejecución basta escribir su nombre.

Escritura y lectura de archivos por líneas.

#define LARGOLINEA 80 //máximo largo de línea igual a 80 caracteres
char buffer[LARGOLINEA];

int lee_archivo(void)
{
    FILE *stream;
   /* Abre stream para lectura, en modo texto. */
    if ((stream = fopen("input.txt", "r")) == NULL) {
       fprintf(stderr, "No pudo abrir archivo de entrada.n");
    return 1;
   }
   while(!feof(stream)) /* lee hasta encontrar el final del stream */
   {
       fgets(buffer, LARGOLINEA, stream); //carga buffer
       if(!feof(stream))
         {

           // Aquí debería procesarse la línea
          //printf("%s ", buffer); //muestra las líneas del archivo de entrada
         }
        else break;
    }
    fclose(stream); /* close stream */
    return 0;

Profesor Leopoldo Silva Bijit                   20-01-2010
24                                                              Estructuras de Datos y Algoritmos
}

int escribe_archivo(void)
{
   FILE *stream;

/* Abre stream para escritura, en modo texto. */
    if ((stream = fopen("output.txt", "w")) == NULL) {
       fprintf(stderr, "No pudo crear archivo de salida.n");
    return 1;
   }

      // Aquí debería escribirse el el archivo..
      //fputs(buffer, stream);        imprime línea
      //fprintf(stream, "%dt", 5); salida formateada
     // fputc('n', stream);          salida caracteres

     fclose(stream); /* close stream */
     return 0;
}

Referencias.

Niklaus Wirth, “Algorithms + Data Structures = Programs”, Prentice-Hall 1975.




Profesor Leopoldo Silva Bijit                  20-01-2010
Introducción a analizadores léxicos                                                                                                                       25



Índice general.

APÉNDICE 3 .............................................................................................................................................. 1
INTRODUCCIÓN A LA ESTRUCTURA Y OPERACIÓN DE ANALIZADORES LÉXICOS. ...... 1
    A3.1. ESTRUCTURA DE UN LENGUAJE DE PROGRAMACIÓN. ...................................................................... 1
    A3.2. ANALIZADOR LÉXICO. (PARSER) ..................................................................................................... 2
    A3.3. REGLAS DE ANÁLISIS. ..................................................................................................................... 4
      Símbolo terminal. ................................................................................................................................ 5
      Símbolo no terminal. ........................................................................................................................... 5
      Alternativa. .......................................................................................................................................... 5
      Concatenación..................................................................................................................................... 6
      Repetición. ........................................................................................................................................... 7
      Resumen. ............................................................................................................................................. 8
    EJEMPLO A3.1. RECONOCEDOR SIMPLE. ................................................................................................... 8
    EJEMPLO A3.2. PARSER BNF. ............................................................................................................... 10
    EJEMPLO A3.3. RECONOCEDOR DE IDENTIFICADOR. .............................................................................. 13
    EJEMPLO A3.4. RECONOCEDOR DE UNA DEFINICIÓN. ............................................................................ 15
    A3.4. MANIPULACIÓN DE ARCHIVOS EN C. ............................................................................................. 18
      Escritura de archivos de texto, con estructura de líneas. .................................................................. 19
      Escritura de archivo, desde un programa. ........................................................................................ 19
      Lectura de archivos de texto con estructura de líneas. ..................................................................... 20
      Llenar un arreglo a partir de un archivo. ......................................................................................... 21
      Escritura y lectura de archivos binarios. .......................................................................................... 22
      Compilación y ejecución en ambiente UNIX. .................................................................................... 23
      Escritura y lectura de archivos por líneas. ....................................................................................... 23
    REFERENCIAS. ........................................................................................................................................ 24
    ÍNDICE GENERAL. ................................................................................................................................... 25
    ÍNDICE DE FIGURAS................................................................................................................................. 25


Índice de figuras.

FIGURA A3.1. ANÁLISIS SIN VOLVER ATRÁS. ................................................................................................ 3
FIGURA A3.2. SÍMBOLO TERMINAL. .............................................................................................................. 5
FIGURA A3.3. SÍMBOLO NO TERMINAL. ........................................................................................................ 5
FIGURA A3.4. ALTERNATIVA. ....................................................................................................................... 6
FIGURA A3.5. ALTERNATIVA, CON PRIMER SÍMBOLO EXPLÍCITO. ................................................................. 6
FIGURA A3.6. CONCATENACIÓN. .................................................................................................................. 7
FIGURA A3.7. REPETICIÓN............................................................................................................................ 7
FIGURA A3.8. REPETICIÓN, CON PRIMER SÍMBOLO EXPLÍCITO. ..................................................................... 7
FIGURA A3.9. GRAFO DEL RECONOCEDOR. ................................................................................................... 8
FIGURA A3.10. ESTADOS DE RECONOCEDOR DE IDENTIFICADOR. ............................................................... 14
FIGURA A3.11. ESTADOS DE RECONOCEDOR DE DEFINICIONES................................................................... 16




Profesor Leopoldo Silva Bijit                                           20-01-2010

Más contenido relacionado

La actualidad más candente

La actualidad más candente (18)

Extremos (3 variables)
Extremos (3 variables)Extremos (3 variables)
Extremos (3 variables)
 
Analisis lexico 2
Analisis lexico 2Analisis lexico 2
Analisis lexico 2
 
Compiladores1
Compiladores1Compiladores1
Compiladores1
 
Analisis lexico
Analisis lexicoAnalisis lexico
Analisis lexico
 
Produccion escrita de_matematicas_2
Produccion escrita de_matematicas_2Produccion escrita de_matematicas_2
Produccion escrita de_matematicas_2
 
Teoria de automatas
Teoria de automatasTeoria de automatas
Teoria de automatas
 
Compiladores, Analisis Lexico
Compiladores, Analisis LexicoCompiladores, Analisis Lexico
Compiladores, Analisis Lexico
 
Matematica
MatematicaMatematica
Matematica
 
Operadores de c_..
Operadores de c_..Operadores de c_..
Operadores de c_..
 
OPERADORES PARA C++
OPERADORES PARA C++OPERADORES PARA C++
OPERADORES PARA C++
 
Automatas[1]
Automatas[1]Automatas[1]
Automatas[1]
 
Algebra de boole libro
Algebra  de   boole  libroAlgebra  de   boole  libro
Algebra de boole libro
 
Teoría de Autómata
Teoría de AutómataTeoría de Autómata
Teoría de Autómata
 
Ejercicios
EjerciciosEjercicios
Ejercicios
 
El álgebra booleana
El álgebra booleanaEl álgebra booleana
El álgebra booleana
 
5 Expresiones
5 Expresiones5 Expresiones
5 Expresiones
 
Braulio yeuris g3
Braulio yeuris g3Braulio yeuris g3
Braulio yeuris g3
 
Operadores Importancia
Operadores ImportanciaOperadores Importancia
Operadores Importancia
 

Similar a Ap3

Presentación 2014 profe gabriel
Presentación 2014 profe gabrielPresentación 2014 profe gabriel
Presentación 2014 profe gabrielEnrique Morales
 
Taller analisis semantico
Taller analisis semanticoTaller analisis semantico
Taller analisis semanticoAlvaro Cedeño
 
Analizador Sintactico
Analizador SintacticoAnalizador Sintactico
Analizador SintacticoBayo Chicaiza
 
Tipos de gramatica y arboles de derivacion
Tipos de gramatica y arboles de derivacionTipos de gramatica y arboles de derivacion
Tipos de gramatica y arboles de derivacionjorge severino
 
6.1 arrays en java
6.1 arrays en java 6.1 arrays en java
6.1 arrays en java Johan Retos
 
Introducción a los compiladores - Parte 2
Introducción a los compiladores - Parte 2Introducción a los compiladores - Parte 2
Introducción a los compiladores - Parte 2Universidad
 
El papel del analizador sintáctico
El papel del analizador sintácticoEl papel del analizador sintáctico
El papel del analizador sintácticoHector Espinosa
 
Cuaderno rojo selectividad matemáticas ccss
Cuaderno rojo selectividad matemáticas ccssCuaderno rojo selectividad matemáticas ccss
Cuaderno rojo selectividad matemáticas ccssKALIUM academia
 
Representaciones estructurales
Representaciones estructuralesRepresentaciones estructurales
Representaciones estructuralesAlvays Rodriguez
 
1.-El-lenguaje-algebraico.pdf
1.-El-lenguaje-algebraico.pdf1.-El-lenguaje-algebraico.pdf
1.-El-lenguaje-algebraico.pdfSimon Perez
 
Recta Numerica
Recta NumericaRecta Numerica
Recta Numericafortiz_jm
 
Automatas
AutomatasAutomatas
Automatasveriyo
 

Similar a Ap3 (20)

Presentación 2014 profe gabriel
Presentación 2014 profe gabrielPresentación 2014 profe gabriel
Presentación 2014 profe gabriel
 
Analisis sintactico
Analisis sintacticoAnalisis sintactico
Analisis sintactico
 
Capitulo 3 paul carrera,dego balcazar
Capitulo 3 paul carrera,dego balcazarCapitulo 3 paul carrera,dego balcazar
Capitulo 3 paul carrera,dego balcazar
 
Análisis Sintáctico
Análisis SintácticoAnálisis Sintáctico
Análisis Sintáctico
 
Taller analisis semantico
Taller analisis semanticoTaller analisis semantico
Taller analisis semantico
 
Automat1111
Automat1111Automat1111
Automat1111
 
Analizador Sintactico
Analizador SintacticoAnalizador Sintactico
Analizador Sintactico
 
Tipos de gramatica y arboles de derivacion
Tipos de gramatica y arboles de derivacionTipos de gramatica y arboles de derivacion
Tipos de gramatica y arboles de derivacion
 
Analisis Semantico
Analisis Semantico Analisis Semantico
Analisis Semantico
 
6.1 arrays en java
6.1 arrays en java 6.1 arrays en java
6.1 arrays en java
 
Introducción a los compiladores - Parte 2
Introducción a los compiladores - Parte 2Introducción a los compiladores - Parte 2
Introducción a los compiladores - Parte 2
 
El papel del analizador sintáctico
El papel del analizador sintácticoEl papel del analizador sintáctico
El papel del analizador sintáctico
 
Ecuaciones lineales diagrama de flujo
Ecuaciones lineales diagrama de flujo Ecuaciones lineales diagrama de flujo
Ecuaciones lineales diagrama de flujo
 
(gramatica atribuida)
(gramatica atribuida)(gramatica atribuida)
(gramatica atribuida)
 
Cuaderno rojo selectividad matemáticas ccss
Cuaderno rojo selectividad matemáticas ccssCuaderno rojo selectividad matemáticas ccss
Cuaderno rojo selectividad matemáticas ccss
 
Representaciones estructurales
Representaciones estructuralesRepresentaciones estructurales
Representaciones estructurales
 
1.-El-lenguaje-algebraico.pdf
1.-El-lenguaje-algebraico.pdf1.-El-lenguaje-algebraico.pdf
1.-El-lenguaje-algebraico.pdf
 
Correlacion y regresion listo
Correlacion y regresion listoCorrelacion y regresion listo
Correlacion y regresion listo
 
Recta Numerica
Recta NumericaRecta Numerica
Recta Numerica
 
Automatas
AutomatasAutomatas
Automatas
 

Ap3

  • 1. 1 Apéndice 3 Introducción a la estructura y operación de analizadores léxicos. A3.1. Estructura de un lenguaje de programación. Un lenguaje está basado en un vocabulario, o léxico, el que está compuesto por palabras, o más precisamente por símbolos. Ciertas secuencias de palabras son reconocidas como sintácticamente bien formadas o correctas. La gramática o sintaxis o estructura del lenguaje queda descrita por una serie de reglas o fórmulas que definen si una secuencia de símbolos es una sentencia correcta. La estructura de las sentencias establece el significado o semántica de ésta. Ejemplo. <sentencia> ::= <sujeto><predicado> <sujeto>::= árboles|arbustos <predicado>::=grandes|pequeños La semántica de las líneas anteriores es la siguiente: Una sentencia está formada por un sujeto seguido de un predicado. Un sujeto es la palabra árboles o arbustos. Un predicado consiste de una sola palabra, la cual puede ser grandes o pequeños. El lenguaje anterior, genera cuatro sentencias correctas. Una sentencia bien formada puede ser derivada a partir del símbolo de partida, <sentencia> en el caso del ejemplo, por la repetida aplicación de reglas de reemplazo o producciones (reglas sintácticas). Se denominan símbolos no terminales, o categorías sintácticas, a las sentencias definidas entre paréntesis de ángulo, que figuran a la derecha en las producciones; los símbolos terminales (vocabulario) figuran a la derecha de las producciones y se representan a sí mismos. Estas reglas para definir lenguajes se denomina formulismo de Backus-Nauer. Los paréntesis de ángulo, y los símbolos ::= (que se lee: puede ser reemplazado por) y | (que se lee como: o excluyente) son denominados metasímbolos. Las producciones son libres al contexto, si en éstas figura a la izquierda un solo símbolo no terminal S, que puede ser reemplazado en función de símbolos terminales s, no importando el contexto en el que ocurra S. Profesor Leopoldo Silva Bijit 20-01-2010
  • 2. 2 Estructuras de Datos y Algoritmos La producción: ASB::=AsB define que S puede ser reemplazado por s, siempre que ocurra entre A y B; por lo cual es sensible al contexto. El uso de la recursión al definir producciones, permite generar un infinito número de sentencias a partir de un número finito de producciones. S::=aA A::=b|cA La categoría A, está definida en términos de sí misma. En el ejemplo, los símbolos terminales se representan con minúsculas, los no terminales con mayúsculas. A partir de S, se generan: ab, acb, accb, acccb, ….. A3.2. Analizador léxico. (parser) El diseño de un reconocedor de sentencias correctas está basado en encontrar algoritmos que sean de complejidad n, donde n es el largo de la sentencia a analizar. Es decir en cada paso del algoritmo se depende solamente del estado actual y del siguiente símbolo; y no es necesario volver atrás. Obviamente la estructura del lenguaje debe permitir esta forma de análisis. El método jerárquico o top-down, de análisis de sentencias (parsing) consiste en reconstruir el árbol de derivación desde el símbolo de partida hasta la sentencia final. Ejemplo: Se da la sentencia: árboles grandes, y se desea determinar si pertenece al lenguaje definido en un ejemplo anterior. Se parte del símbolo de partida, <sentencia> y se lee el primer símbolo del texto que se desea analizar: árboles. Se reemplaza <sentencia> por <sujeto><predicado>, se ve si es posible reemplazar <sujeto>; se verifica que puede ser reemplazado por árboles. En este momento, puede avanzarse al siguiente símbolo de la secuencia de entrada, que en el caso del ejemplo es grandes. Al mismo tiempo se avanza al siguiente de los símbolos no terminales. Ahora la tarea restante es verificar si <predicado> puede generar el símbolo grandes. Como esto es así, se avanza en la secuencia de entrada, y se observa que no quedan más símbolos. Con lo cual el análisis termina reconociendo cómo válida la construcción. Cada paso del análisis está basado solamente en el siguiente símbolo de la secuencia de símbolos no terminales que se está analizando. Puede ilustrarse el algoritmo mediante la siguiente tabla. La columna a la izquierda representa la tarea de reconocimiento pendiente y la de la derecha la secuencia de símbolos terminales que aún no se leen. Se desea validar accb como perteneciente a S. S::=aA A::=b|cA Profesor Leopoldo Silva Bijit 20-01-2010
  • 3. Introducción a analizadores léxicos 3 S accb Inicio. Se lee a aA accb Se reemplaza S. Se reconoce a. A ccb Se acepta a. Se lee c cA ccb Se reemplaza A. Se reconoce c. A cb Se acepta c. Se lee c. cA cb Se reemplaza A. Se reconoce c. A b Se acepta c. Se lee b. b b Se reemplaza A. Se reconoce b. - - Se acepta la frase como correcta. Figura A3.1. Análisis sin volver atrás. No es necesario volver atrás, y el análisis está basado en la lectura de un símbolo terminal por adelantado. Para que esto sea posible, los símbolos iniciales de símbolos no terminales alternativos que figuran a la derecha en las producciones, deben ser diferentes. El siguiente ejemplo ilustra reglas que no cumplen el principio anterior. Ya que A y B (símbolos no terminales alternativos en S), tienen iguales símbolos iniciales, ambos son x. S::=A|B A::=xA|y B::=xB|z Si se desea analizar la secuencia xxxz, se tendrá la dificultad que no es posible discernir (sólo leyendo el primer símbolo por adelantado) si S debe ser reemplazado por A o por B. Si se eligiera al azar, reemplazar S por A, luego de unos pasos el análisis falla, y se debería volver atrás, e intentar reemplazar por B, en lugar de A, y volver a intentar. Las reglas: S::=C|xS C::=y|z Son equivalentes a las anteriores y cumplen el principio anterior. En variadas construcciones de los lenguajes se acepta símbolos opcionales. Es decir la alternativa entre un símbolo terminal y una secuencia nula de símbolos. Ejemplo de esto es la asignación: x= +a; el símbolo + es opcional. Sea nula la secuencia nula. Entonces las reglas: S::=Ax A::=x|nula Si se desea reconocer x, si luego de reemplazar S por Ax, se intenta reemplazar A por x el análisis falla, se logra xx en la derivación. Con lo cual puede reconocerse el primer x, y luego al Profesor Leopoldo Silva Bijit 20-01-2010
  • 4. 4 Estructuras de Datos y Algoritmos intentar leer el próximo, como no quedan símbolos no terminales que leer y queda pendiente un x que derivar, se concluye que el análisis falló. Lo que se debió realizar era reemplazar A por nula. Para evitar la vuelta atrás en el reconocimiento, se impone una regla adicional para las producciones que generen la secuencia nula: Para una secuencia A que genera la secuencia nula, los símbolos iniciales que genera A deben se disjuntos con los símbolos que siguen a cualquier secuencia generada por A. En el ejemplo anterior, S dice que la sentencia A tiene a x como símbolo siguiente. Y la producción que define A, indica que el primer símbolo que puede generar A es también x. Como los iniciales generados por A son iguales a los siguientes a A, se viola la regla anterior. La repetición de construcciones, que también es muy frecuente en los lenguajes, suele definirse empleando recursión. Por ejemplo la repetición de una o más veces del elemento B, puede anotarse: A::= B|AB Pero el primero de B y el primero de AB no es el vacío, y no cumple la primera de las reglas. Si se cambia la definición de A por: A::=nula|AB A genera: nula, B, BB, BBB, … y se tendrá que el primero de A y el siguiente a A serán B, violando la segunda regla. Lo cual permite visualizar que no puede emplearse recursión por la izquierda. La recursión por la derecha, cumple las reglas anteriores: A::=nula|BA Esta última producción también genera la repetición de cero, una o más veces del elemento B. La frecuencia de construcciones repetitivas que generen la secuencia nula lleva a definir los siguiente metasímbolos: A::={B} Que genera: nula, B, BB, BBB, … Esta definición sólo simplifica la notación, pero aún es preciso revisar que se cumpla la segunda regla, para emplear algoritmos basados en leer un símbolo por adelantado y sin volver atrás. A3.3. Reglas de análisis. Debido a lo abstracto del formalismo de Backus-Nauer se ha desarrollado una representación gráfica de las reglas. En los grafos sintéticos se emplean los siguientes símbolos: Profesor Leopoldo Silva Bijit 20-01-2010
  • 5. Introducción a analizadores léxicos 5 Símbolo terminal. x Figura A3.2. Símbolo terminal. Corresponde a un reconocimiento del símbolo terminal x, en la producción de la que forma parte, y al avanzar en la lectura del siguiente símbolo en la secuencia de entrada. Es importante asociar estos grafos a elementos de un lenguaje de programación que implementará el reconocedor sintáctico basado en diagramas. Para el elemento terminal, puede traducirse: if (ch== „x‟) ch=lee(stream); else error(n); Donde stream es el descriptor del archivo que contiene el texto que será analizado. La función lee, trae el siguiente carácter. En este nivel los caracteres individuales del texto se consideran símbolos terminales. La función de error, debería generar un mensaje asociado a la detección de una sentencia mal formada. Símbolo no terminal. B Figura A3.3. Símbolo no terminal. Cuando aparece este diagrama en una producción, corresponde a la activación de un reconocedor del símbolo no terminal B. En el reconocedor, se activa una invocación al procedimiento reconocedor de B. B( ); Alternativa. La producción: A::=B1|B2|…|Bn Se representa: Profesor Leopoldo Silva Bijit 20-01-2010
  • 6. 6 Estructuras de Datos y Algoritmos B1 A B2 Bn Figura A3.4. Alternativa. En el reconocedor, puede implementarse, mediante la sentencia switch. switch (ch){ case L1: B1(); break; case L2: B2(); break; …. case Ln: Bn(); break; } Donde los Li serían los conjuntos de los símbolos iniciales de los Bi. Es preferible describir la alternativa, explicitando el primer símbolo: b1 B1 A b2 B2 bn Bn Figura A3.5. Alternativa, con primer símbolo explícito. switch (ch){ case „b1‟: {ch=lee(stream); B1(); break;} case „b2‟: {ch=lee(stream); B2(); break;} …. case „bn‟: {ch=lee(stream); Bn(); break;} } Concatenación. La producción: A::=B1B2…Bn Se representa: Profesor Leopoldo Silva Bijit 20-01-2010
  • 7. Introducción a analizadores léxicos 7 A B1 B2 Bn Figura A3.6. Concatenación. En el reconocedor: {B1( ); B2( );…Bn( );} Repetición. La producción: A::={B} Se representa: A B Figura A3.7. Repetición. En el reconocedor, se implementa: while( esta_en(L, ch) ) B( ); Donde la función esta_en retorna verdadero si ch pertenece al conjunto L de los primeros caracteres generados por B. Es preferible, representar, el forma explícita el primer carácter: A B b Figura A3.8. Repetición, con primer símbolo explícito. De este modo el reconocedor se implementa: while( ch==‟b‟) {ch=lee(stream); B( );} La repetición, de a lo menos una vez, puede implementarse con una sentencia while. Cada uno de los bloques anteriores puede ser reemplazado por alguna de las construcciones anteriores. Por ejemplo: la repetición puede ser una serie de acciones concatenadas. Profesor Leopoldo Silva Bijit 20-01-2010
  • 8. 8 Estructuras de Datos y Algoritmos Resumen. Para una gramática dada, pueden construirse grafos sintácticos a partir de las producciones descritas en BNF, y viceversa. Y de éstas derivar el código del reconocedor. Los grafos deben cumplir las siguientes dos reglas, para que se puedan recorrer leyendo un símbolo por adelantado y sin volver atrás. Los primeros símbolos en las alternativas deben ser diferentes. De tal forma que la bifurcación solo pueda escogerse observando el siguiente símbolo de esa rama. Si un grafo reconocedor de una sentencia A, puede generar la secuencia nula, debe rotularse con todos los símbolos que puedan seguir a A. Ya que ingresar al lazo puede afectar el reconocimiento de lo que viene a continuación. Una vez definido el lenguaje a reconocer, mediante sus grafos, debe verificarse el cumplimiento de las dos reglas anteriores. Un sistema de grafos que cumplan las reglas anteriores se denomina determinista y puede ser recorrido sin volver atrás y solo leyendo un símbolo por adelantado. Esta restricción no es una limitante en los casos prácticos. Ejemplo A3.1. Reconocedor simple. Generar reconocedor para sentencias que cumplan las siguientes producciones. A::=x|(B) B::=AC C::={+A} Algunas sentencias válidas, de este lenguaje, son: x, (x), (x+x), ((x)), (x+x+x+x+x+x+x+x+x),…. Pueden plantearse los grafos sintácticos para cada una de las producciones. Posteriormente, es posible reducir los grafos, mediante substituciones. Luego de esto se obtiene: A ( A ) A + x Figura A3.9. Grafo del reconocedor. El grafo permite revisar el cumplimiento de las dos reglas. La bifurcación tiene intersección vacía de los primeros elementos de cada rama: { „(„ } { „x‟ } = Profesor Leopoldo Silva Bijit 20-01-2010
  • 9. Introducción a analizadores léxicos 9 La producción que genera la secuencia nula tiene intersección vacía del primer elemento de la repetición y del símbolo que sigue a esa repetición: { „+„ } { „)‟ } = El siguiente programa implementa un reconocedor, para sentencias que cumplan la sintaxis descrita por el grafo. Lo más importante es notar que el código para el reconocedor de sentencias A, puede ser escrito a partir del diagrama anterior. Cada conjunto de reglas da origen a un programa determinado. Se han agregado las funciones que abren y leen el archivo con el texto que será analizado, para ilustrar los detalles del entorno. Se da término a las sentencias del archivo con un asterisco, en la primera posición de una línea. Cada vez que termina el análisis de una sentencia avisa si la encontró correcta. Se emplea una variable global ch, para disminuir el número de argumentos. Se destaca que debe leerse un símbolo por adelantado, antes de invocar al reconocedor. Si el descriptor del archivo se deja como variable global, pueden disminuirse aún más los argumentos de las funciones, simplificándolas. #include <stdio.h> void error(int e) { printf("Error %dn", e);} char lee(FILE *stream) { return( fgetc(stream)); } char ch='0'; void A(FILE *stream) { if (ch=='x') ch=lee(stream); else if (ch=='(' ) { ch=lee(stream); A(stream); while(ch=='+') {ch=lee(stream); A(stream);} if ( ch==')' ) ch=lee(stream); else error(1); } else error(2); } /* Analiza archivo de texto */ int parser(void) { FILE *stream; if ((stream = fopen("inparser.txt", "r")) == NULL) { fprintf(stderr, "No pudo abrir archivo de entrada.n"); return 1; Profesor Leopoldo Silva Bijit 20-01-2010
  • 10. 10 Estructuras de Datos y Algoritmos } /* lee hasta encontrar el final del stream */ while(!feof(stream)) { ch=lee(stream); if(ch=='*') break; //lee ch por adelantado. A(stream); printf("%sn","ok"); } printf("%sn","fin"); fclose(stream); return 0; } int main(void) { parser(); return 0; } Ejemplo A3.2. Parser BNF. Se desea reconocer sentencias descritas por la siguiente gramática: producción ::= <símbolo no terminal> „=‟ <expresión> „.‟ expresión ::= <término> {„,‟ <termino>} término ::= <factor> { <factor> } factor ::= <símbolo terminal> | <símbolo no terminal> | „( „ <expresión> „)‟ símbolo no terminal ::= Letra mayúscula símbolo terminal ::= Letra minúscula Los símbolos terminales requeridos por las reglas se han colocado entre comillas simple. Nótese que cada producción termina en el carácter punto. La serie de producciones se termina cuando se encuentra un asterisco: <texto de programa> ::= {producción} „*‟ El parser genera algunos comentarios de error, hacia la salida estándar, indicando la línea y la posición del carácter que no cumple las reglas. #include <stdio.h> #include <stdlib.h> //malloc #include <string.h> #include <ctype.h> void expresion(void); //prototipo. Factor requiere expresión. Profesor Leopoldo Silva Bijit 20-01-2010
  • 11. Introducción a analizadores léxicos 11 char simbolo='0'; int nl=1; //contador de líneas int nc=0; //contador de caracteres en la línea. FILE *stream; void error(int tipo) { putchar('n');printf("(%d,%d): ",nl, nc+1); switch (tipo) { case 1: printf("%sn", "Esperaba símbolo no terminal");break; case 2: printf("%sn", "Esperaba signo igual"); break; case 3: printf("%sn", "Esperaba cierre paréntesis"); break; case 4: printf("%sn", "Esperaba abre paréntesis"); break; case 5: printf("%sn", "Esperaba punto"); break; } } void getch(void) { if(!feof(stream)) { simbolo = fgetc(stream); nc++; if(símbolo == 'n') {nl++; nc=0;} putchar(simbolo); //eco en salida estándard } } void getsimbolo(void) { getch(); while(isspace(simbolo)) getch(); //descarta blancos } //factor ::= <símbolo terminal> | <símbolo no terminal> | „( „ <expresión> „)‟ void factor(void) { if (isalpha(simbolo) ) getsimbolo(); else if (simbolo == '(' ) { getsimbolo(); expresion(); if(símbolo == ')' ) getsimbolo(); else error(3); } else error(4); } Profesor Leopoldo Silva Bijit 20-01-2010
  • 12. 12 Estructuras de Datos y Algoritmos // término ::= <factor> { <factor> } void termino(void) { factor(); while( (isalpha(simbolo)) || (símbolo == '(' ) ) factor(); } Notar que la repetición de factor es precedida por la revisión de los primeros caracteres de factor: es decir que sea un símbolo terminal o no terminal o el comienzo de una expresión, que debe comenzar por paréntesis abierto. //expresión ::= <término> {„,‟ <termino>} void expresion(void) { termino(); while(símbolo == ',') {getsimbolo(); termino();} } // producción ::= <símbolo no terminal> „=‟ <expresión> „.‟ void produccion() { if(isupper(simbolo)) getsimbolo(); else error(1); if (símbolo == '=') getsimbolo(); else error(2); expresion(); if (simbolo != '.') error(5); } /* Lectura de archivo de texto */ int bnfparser(void) { /* Abre stream para lectura, en modo texto. */ if ((stream = fopen("bnfparser.txt", "r")) == NULL) { fprintf(stderr, "No pudo abrir archivo de entrada.n"); return 1; } /* lee hasta encontrar el final del stream */ while(!feof(stream)) { getsimbolo(); if(simbolo=='*') break; produccion(); } printf("%s numero de lineas =%dn","fin de archivo", nl); fclose(stream); /* close stream */ return 0; } Profesor Leopoldo Silva Bijit 20-01-2010
  • 13. Introducción a analizadores léxicos 13 int main(void) { bnfparser(); return 0; } Para el siguiente archivo de entrada: A = C. B=x,A. B=x,A,B,C- C=x(B,D. D=(A). * Se genera la siguiente salida: A = C. B=x,A. B=x,A,B,C- (3,10): Esperaba punto C=x(B,D. (4,8): Esperaba cierre paréntesis D=(A). *fin de archivo número de líneas =5 Ejemplo A3.3. Reconocedor de identificador. Un identificador es una secuencia de caracteres, donde el primero debe ser letra, y los que siguen letras o números. Un identificador puede aparecer entre espacios, tabs, retornos; o estar entre caracteres no alfanuméricos. Si ab y cd son identificadores, las siguientes líneas muestran posibles instancias de éstos: ab (ab + cd) ab= cd; ab = cd+5; Una alternativa al diseño de reconocedores es el diseño basado en diagramas de estados. Se ilustra un ejemplo, basado en análisis de líneas. Más adelante en A3.4 se esbozan procedimientos para leer archivos de texto por líneas. El diagrama de estados de la Figura A3.10, muestra que deben descartarse los espacios (se simboliza por el círculo 0), y comenzar a almacenar el identificador, cuando se encuentra una Profesor Leopoldo Silva Bijit 20-01-2010
  • 14. 14 Estructuras de Datos y Algoritmos letra; luego se siguen almacenado los caracteres del identificador (círculo 1) hasta que llegue un carácter no alfanumérico, en que se vuelve a esperar identificadores. Si lo único que se desea es extraer los identificadores, si no llega una letra cuando se espera una, puede descartársela y continuar el análisis. Si es letra Si es espacio Si es alfanumérico 0 1 No es letra Si no es alfanumérico Figura A3.10. Estados de reconocedor de identificador. Asumiendo que se tiene una línea almacenada en buffer, la siguiente función forma en el string id, el identificador. La estructura del código está basada en el diagrama anterior, y en las funciones cuyos prototipos se encuentran en ctype.h #define LARGOLINEA 80 #define Esperando_letra 0 #define Almacenando_id 1 getword(char *buffer, int nl) { char id[LARGOLINEA]; int i, j, estado; for(i=0, estado=0, j=0; i<strlen(buffer); i++) { switch (estado){ case Esperando_letra: if (isspace(buffer[i]) ) continue; else if (isalpha(buffer[i])) { estado=Almacenando_id; id[j]=buffer[i]; j++; } else ; //No es letra. Descarta char. Profesor Leopoldo Silva Bijit 20-01-2010
  • 15. Introducción a analizadores léxicos 15 break; case Almacenando_id: if (isalnum(buffer[i])) {id[j]=buffer[i]; j++;} //forma id else if(!isalnum(buffer[i])) { estado = Esperando_letra; id[j]='0'; //termina string j=0; //reset posición //printf("%s %dn", id, nl); //muestra los identificadores y la línea // Aquí debería hacerse algo con el identificador root=insert(id, root, nl); //Ejemplo: lo inserta en árbol } break; } } } Ejemplo A3.4. Reconocedor de una definición. El siguiente ejemplo es una elaboración del anterior, y su código se realiza apoyándose en el diagrama de estados de la Figura A3.11. Se desea analizar un texto de programa y reconocer el identificador y su definición que figuran en una línea que comienza con #define. En el siguiente diagrama de estados, se pasa al estado 3, si se reconoce el identificador define, luego del símbolo #. A la salida del estado 4, se tiene el identificador para el cual se está definiendo una equivalencia. A la salida del estado 6, se tiene el identificador con el valor. Luego del estado 6 debe regresar al estado 0. Se emplean las definiciones: #define LARGOID 20 //máximo largo identificadores #define EsperaID 0 //estados #define GetId 1 #define BuscaDefine 2 #define EsperaDef 3 #define GetDef 4 #define EsperaEquiv 5 #define GetEquiv 6 Profesor Leopoldo Silva Bijit 20-01-2010
  • 16. 16 Estructuras de Datos y Algoritmos alfanum 6 isspace 5 !alfanum alfanum !alfanum es alfanumérico id ==”define” 3 4 2 alfanum Es # id != “define” Si es espacio alfanum Si es letra Si es alfanumérico Si es espacio 0 1 No es letra ni # Si no es alfanumérico Figura A3.11. Estados de reconocedor de definiciones. La función puede escribirse, basándose en el diagrama: FILE *streami,*streamo; char ch; #define p() putc(ch,streamo); #define g() ch=getc(streami) void parser(void) { char word[LARGOID]; char equiv[LARGOID]; int j, state; pcelda p; for(state=0, j=0;!feof(streami);g()) { switch (state){ case EsperaID: if (isspace(ch) ) { p(); continue;} else if (isalpha(ch)) {state=GetId; word[j++]=ch;} else if(ch=='#'){state=BuscaDefine; j=0;} else p(); //se traga hasta final de línea break; case GetId: if (isalnum(ch)) {word[j++]=ch;} else { if(!isalnum(ch)) Profesor Leopoldo Silva Bijit 20-01-2010
  • 17. Introducción a analizadores léxicos 17 {state=EsperaID; word[j]='0'; j=0; //printf("%sn",word); if( (p=buscar(word))!=NULL ) //busca identificador { //printf("%s -> %sn", p->definicion, p->equivalencia); fprintf(streamo, p->equivalencia); //lo reeemplaza } else fprintf(streamo,word); } p(); } break; case BuscaDefine: if (isalnum(ch)) {word[j++]=ch;} else if(!isalnum(ch)) {word[j]='0';j=0; if (strcmp(word,"define")==0) { state=EsperaDef; //printf("pre=%sn",word); } else {state=EsperaID; putc('#', streamo); fprintf(streamo,word);p();} } break; case EsperaDef: if (isspace(ch) ) continue; else if (isalpha(ch)) {state=GetDef; word[j++]=ch;} break; case GetDef: if (isalnum(ch)) {word[j++]=ch;} else if(!isalnum(ch)) {state=EsperaEquiv; word[j]='0'; j=0; //printf("Definición =%sn",word); } break; case EsperaEquiv: if (isspace(ch) ) continue; else if (isgraph(ch)) {state=GetEquiv; equiv[j++]=ch;} break; case GetEquiv: if (isgraph(ch)) {equiv[j++]=ch;} else if(!isgraph(ch)) { state=EsperaID; equiv[j]='0';j=0; //printf("insertar valor equivalente en tabla=%sn", equiv); // Aquí debería insertar la palabra word y su equivalencia. if( (p=buscar(word))!=NULL ) descarte(word); //permite redefinición inserte(word, equiv); Profesor Leopoldo Silva Bijit 20-01-2010
  • 18. 18 Estructuras de Datos y Algoritmos } break; } } } El siguiente segmento, abre los archivos de entrada y salida. int procesa_archivos(void) { /* Abre stream para lectura, en modo texto. */ if ((streami = fopen("input6.c", "r")) == NULL) { fprintf(stderr, "No pudo abrir archivo de entrada.n"); return 1; } /* Abre stream para escritura, en modo texto. */ if ((streamo = fopen("output6.c", "w")) == NULL) { fprintf(stderr, "No pudo abrir archivo de salida.n"); return 1; } /* lee hasta encontrar el final del stream */ while(!feof(streami)) { g(); //lee uno por adelantado if(!feof(streami)) { parser(); //putchar(ch); } else break; } fclose(streami); /* close stream */ fclose(streamo); return 0; } int main(void) { makenull(); procesa_archivos(); return 0; } A3.4. Manipulación de archivos en C. En la etapa de prueba de los algoritmos es necesario ingresar datos. Si éstos son numerosos es recomendable efectuar esta operación leyendo los datos desde un archivo. Profesor Leopoldo Silva Bijit 20-01-2010
  • 19. Introducción a analizadores léxicos 19 También es preciso poder extraer resultados, para posterior análisis, escribiéndolos en un archivo. Escritura de archivos de texto, con estructura de líneas. Consideremos primero la escritura de archivos de texto con formato. Esto puede lograrse con un editor, siguiendo ciertas convenciones para separar los elementos de datos. Es recomendable ingresar por líneas, y en cada línea separar los ítems mediante espacios o tabs. Es importante saber el número y tipos de datos de los ítems de cada línea, ya que debe confeccionarse un string de formato con dicha estructura. En caso de escritura de un archivo, mediante un programa, también debe tenerse en cuenta la estructura de la línea. Veamos un ejemplo. Se tiene un entero y un carácter por línea, que configuran un dato con la siguiente estructura: struct mystruct { int i; char ch; }; La confección con un editor, podría generar el siguiente texto, donde los espacios previos al número pueden ser reemplazados por varios espacios o tabs. La separación entre el número y el carácter debe ser un espacio o un tab. Luego pueden existir varios espacios o tabs, seguidos de un retorno. 11 a 22 b 333 c 4d 5e Escritura de archivo, desde un programa. La manipulación de archivos, requiere la invocación a funciones de <stdio.h>, para abrir, cerrar, leer o escribir en el archivo. El siguiente segmento abre para escritura el archivo testwr.txt, que debe estar ubicado en el mismo directorio que el programa ejecutable; en caso de otra ubicación, es preciso preceder el nombre con el sendero de la ruta. El modo “w” establece modo escritura, es decir sobreescribe si el archivo existe, y lo crea en caso contrario. La variable que permite manipular un archivo es de tipo FILE, y se la define según: Profesor Leopoldo Silva Bijit 20-01-2010
  • 20. 20 Estructuras de Datos y Algoritmos FILE *stream; Se suele proteger la apertura, en caso de falla, mediante: if ((stream = fopen("testwr.txt", "w")) == NULL) { fprintf(stderr, "No puede abrir archivo de salida.n"); return 1; } El stream o flujo de salida stderr suele ser la pantalla. La siguiente instrucción escribe en el stream 4 caracteres por línea, más el terminador de línea, eol; que suele ser uno o dos caracteres. La estructura de la línea: dd<sp>c<eol>. fprintf(stream, "%2d %cn", s.i, s.ch); Una vez completada la escritura de todas las líneas, se cierra el archivo, mediante: fclose(stream); Lectura de archivos de texto con estructura de líneas. Se incluye un programa para leer el archivo, ya sea generado con un editor o por un programa, mediante la función fscanf. Se ha usado la función feof, para determinar si se llegó al final del archivo. La acción que se realiza con los datos es simplemente desplegar en la pantalla, los datos formateados. /* Ejemplo con streams. Lectura de archivo de texto formateado */ #include <stdio.h> //El archivo testwr.txt debe estar estructurado en líneas. //Cada línea debe estar formateada según: <sep>entero<sep>char<eol> //Donde <sep> pueden ser espacios o tabs. //El entero debe ser de dos dígitos, si en el string de formato del fscanf figura como %2d. FILE *stream; int main(void) { int jj; char cc; /* Abre stream para lectura, en modo texto. */ if ((stream = fopen("testwr.txt", "r")) == NULL){ fprintf(stderr, "No pudo abrir archivo de entrada.n"); return 1; } /* lee hasta encontrar el final del stream */ Profesor Leopoldo Silva Bijit 20-01-2010
  • 21. Introducción a analizadores léxicos 21 for(;;) { fscanf(stream, "%d %c", &jj, &cc); //lee variables según su tipo y estructura de línea. if(feof(stream)) break; printf("%d %cn", jj, cc); //sólo muestra las líneas del archivo } fclose(stream); /* close stream */ return 0; } Si se intenta leer más allá del fin de archivo la función feof, retorna verdadero. Llenar un arreglo a partir de un archivo. El siguiente ejemplo llena un arreglo de estructuras. /* Ejemplo con streams. Con datos de archivo se escribe un arreglo */ #include <stdio.h> //El archivo testwr.txt debe estar estructurado en lineas. //Cada linea debe estar formateada según: <sep>entero<sep>char<eol> //Donde <sep> pueden ser espacios o tabs. //El entero debe ser de dos dígitos, si en el string de formato del fscanf figura como %2d. struct mystruct { int i; char ch; }; #define ITEMS 20 struct mystruct arr[ITEMS]; FILE *stream; int main(void) { int i, jj; char cc; /* Abre stream para lectura, en modo texto. */ if ((stream = fopen("testwr.txt", "r")) == NULL) { fprintf(stderr, "No pudo abrir archivo de entrada.n"); return 1; } for(i=0; i< ITEMS; i++) { fscanf(stream, "%d %c", &jj, &cc); //lee variables según tipo. if(feof(stream)) break; arr[i].i=jj; arr[i].ch=cc; //llena items del arreglo } Profesor Leopoldo Silva Bijit 20-01-2010
  • 22. 22 Estructuras de Datos y Algoritmos fclose(stream); /* close stream */ for(i=0;i< ITEMS;i++) { printf("%d %cn", arr[i].i, arr[i].ch);// muestra el arreglo } return 0; } Escritura y lectura de archivos binarios. El siguiente ejemplo, ilustra la escritura y lectura de archivos binarios, no de texto. Se emplean ahora las funciones: fwrite, fseek y fread. * Ejemplo con streams. Escritura y luego lectura de archivo binario */ #include <stdio.h> struct mystruct { int i; char ch; }; int main(void) { FILE *stream; struct mystruct s; int j; /* sobreescribe y lo abre para escritura o lectura en modo binario */ if ((stream = fopen("TEST.bin", "w+b")) == NULL) { fprintf(stderr, "No se puede crear archivo de salida.n"); return 1; } for(j=0;j<20;j++) { s.i = j; s.ch = 'A'+j; fwrite(&s, sizeof(s), 1, stream); /* write struct s to file */ } /* seek to the beginning of the file */ fseek(stream, SEEK_SET, 0); /* lee y despliega los datos */ Profesor Leopoldo Silva Bijit 20-01-2010
  • 23. Introducción a analizadores léxicos 23 for(j=0; j<20;j++) { fread(&s, sizeof(s), 1, stream); printf("%d %cn", s.i, s.ch); } fclose(stream); /* close file */ return 0; } El archivo TEST.bin, no puede ser visualizado con un editor de texto. Para su interpretación debe usarse un editor binario. Compilación y ejecución en ambiente UNIX. Para programas sencillos, como los ilustrados, puede generarse el ejecutable en ambiente UNIX, mediante el comando: make <nombre de archivo c, sin extensión> Esto crea un ejecutable de igual nombre al programa en C. Para su ejecución basta escribir su nombre. Escritura y lectura de archivos por líneas. #define LARGOLINEA 80 //máximo largo de línea igual a 80 caracteres char buffer[LARGOLINEA]; int lee_archivo(void) { FILE *stream; /* Abre stream para lectura, en modo texto. */ if ((stream = fopen("input.txt", "r")) == NULL) { fprintf(stderr, "No pudo abrir archivo de entrada.n"); return 1; } while(!feof(stream)) /* lee hasta encontrar el final del stream */ { fgets(buffer, LARGOLINEA, stream); //carga buffer if(!feof(stream)) { // Aquí debería procesarse la línea //printf("%s ", buffer); //muestra las líneas del archivo de entrada } else break; } fclose(stream); /* close stream */ return 0; Profesor Leopoldo Silva Bijit 20-01-2010
  • 24. 24 Estructuras de Datos y Algoritmos } int escribe_archivo(void) { FILE *stream; /* Abre stream para escritura, en modo texto. */ if ((stream = fopen("output.txt", "w")) == NULL) { fprintf(stderr, "No pudo crear archivo de salida.n"); return 1; } // Aquí debería escribirse el el archivo.. //fputs(buffer, stream); imprime línea //fprintf(stream, "%dt", 5); salida formateada // fputc('n', stream); salida caracteres fclose(stream); /* close stream */ return 0; } Referencias. Niklaus Wirth, “Algorithms + Data Structures = Programs”, Prentice-Hall 1975. Profesor Leopoldo Silva Bijit 20-01-2010
  • 25. Introducción a analizadores léxicos 25 Índice general. APÉNDICE 3 .............................................................................................................................................. 1 INTRODUCCIÓN A LA ESTRUCTURA Y OPERACIÓN DE ANALIZADORES LÉXICOS. ...... 1 A3.1. ESTRUCTURA DE UN LENGUAJE DE PROGRAMACIÓN. ...................................................................... 1 A3.2. ANALIZADOR LÉXICO. (PARSER) ..................................................................................................... 2 A3.3. REGLAS DE ANÁLISIS. ..................................................................................................................... 4 Símbolo terminal. ................................................................................................................................ 5 Símbolo no terminal. ........................................................................................................................... 5 Alternativa. .......................................................................................................................................... 5 Concatenación..................................................................................................................................... 6 Repetición. ........................................................................................................................................... 7 Resumen. ............................................................................................................................................. 8 EJEMPLO A3.1. RECONOCEDOR SIMPLE. ................................................................................................... 8 EJEMPLO A3.2. PARSER BNF. ............................................................................................................... 10 EJEMPLO A3.3. RECONOCEDOR DE IDENTIFICADOR. .............................................................................. 13 EJEMPLO A3.4. RECONOCEDOR DE UNA DEFINICIÓN. ............................................................................ 15 A3.4. MANIPULACIÓN DE ARCHIVOS EN C. ............................................................................................. 18 Escritura de archivos de texto, con estructura de líneas. .................................................................. 19 Escritura de archivo, desde un programa. ........................................................................................ 19 Lectura de archivos de texto con estructura de líneas. ..................................................................... 20 Llenar un arreglo a partir de un archivo. ......................................................................................... 21 Escritura y lectura de archivos binarios. .......................................................................................... 22 Compilación y ejecución en ambiente UNIX. .................................................................................... 23 Escritura y lectura de archivos por líneas. ....................................................................................... 23 REFERENCIAS. ........................................................................................................................................ 24 ÍNDICE GENERAL. ................................................................................................................................... 25 ÍNDICE DE FIGURAS................................................................................................................................. 25 Índice de figuras. FIGURA A3.1. ANÁLISIS SIN VOLVER ATRÁS. ................................................................................................ 3 FIGURA A3.2. SÍMBOLO TERMINAL. .............................................................................................................. 5 FIGURA A3.3. SÍMBOLO NO TERMINAL. ........................................................................................................ 5 FIGURA A3.4. ALTERNATIVA. ....................................................................................................................... 6 FIGURA A3.5. ALTERNATIVA, CON PRIMER SÍMBOLO EXPLÍCITO. ................................................................. 6 FIGURA A3.6. CONCATENACIÓN. .................................................................................................................. 7 FIGURA A3.7. REPETICIÓN............................................................................................................................ 7 FIGURA A3.8. REPETICIÓN, CON PRIMER SÍMBOLO EXPLÍCITO. ..................................................................... 7 FIGURA A3.9. GRAFO DEL RECONOCEDOR. ................................................................................................... 8 FIGURA A3.10. ESTADOS DE RECONOCEDOR DE IDENTIFICADOR. ............................................................... 14 FIGURA A3.11. ESTADOS DE RECONOCEDOR DE DEFINICIONES................................................................... 16 Profesor Leopoldo Silva Bijit 20-01-2010