Está en la página 1de 25

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 26-05-2008


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 26-05-2008


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
intentar leer el próximo, como no quedan símbolos no terminales que leer y queda pendiente un

Profesor Leopoldo Silva Bijit 26-05-2008


4 Estructuras de Datos y Algoritmos
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 26-05-2008


Introducción a analizadores léxicos 5

Símbolo terminal.

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.

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 26-05-2008


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 26-05-2008


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:

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:

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.

Resumen.

Para una gramática dada, pueden construirse grafos sintácticos a partir de las producciones
descritas en BNF, y viceversa.

Profesor Leopoldo Silva Bijit 26-05-2008


8 Estructuras de Datos y Algoritmos
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 +

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‟ } =

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:
{ „+„ } { „)‟ } =

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción a analizadores léxicos 9

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 %d\n", 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;
}

/* lee hasta encontrar el final del stream */

Profesor Leopoldo Silva Bijit 26-05-2008


10 Estructuras de Datos y Algoritmos
while(!feof(stream))
{
ch=lee(stream); if(ch=='*') break; //lee ch por adelantado.
A(stream);
printf("%s\n","ok");
}
printf("%s\n","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.

char simbolo='\0';
int nl=1; //contador de líneas
int nc=0; //contador de caracteres en la línea.

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción a analizadores léxicos 11
FILE *stream;

void error(int tipo)


{
putchar('\n');printf("(%d,%d): ",nl, nc+1);
switch (tipo)
{
case 1: printf("%s\n", "Esperaba símbolo no terminal");break;
case 2: printf("%s\n", "Esperaba signo igual"); break;
case 3: printf("%s\n", "Esperaba cierre paréntesis"); break;
case 4: printf("%s\n", "Esperaba abre paréntesis"); break;
case 5: printf("%s\n", "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);
}

// término ::= <factor> { <factor> }


void termino(void)
{

Profesor Leopoldo Silva Bijit 26-05-2008


12 Estructuras de Datos y Algoritmos
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 =%d\n","fin de archivo", nl);

fclose(stream); /* close stream */


return 0;
}

int main(void)
{
bnfparser();

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción a analizadores léxicos 13

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
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.

Profesor Leopoldo Silva Bijit 26-05-2008


14 Estructuras de Datos y Algoritmos
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.
break;
case Almacenando_id:
if (isalnum(buffer[i]))

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción a analizadores léxicos 15
{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 %d\n", 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 26-05-2008


16 Estructuras de Datos y Algoritmos

alfanum
6 5 isspace
!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))
{state=EsperaID; word[j]='\0'; j=0;

Profesor Leopoldo Silva Bijit 26-05-2008


Introducción a analizadores léxicos 17
//printf("%s\n",word);
if( (p=buscar(word))!=NULL ) //busca identificador
{
//printf("%s -> %s\n", 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=%s\n",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 =%s\n",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=%s\n", 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 26-05-2008


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 26-05-2008


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 26-05-2008


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 %c\n", 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 26-05-2008


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 %c\n", 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 26-05-2008


22 Estructuras de Datos y Algoritmos

fclose(stream); /* close stream */

for(i=0;i< ITEMS;i++)
{
printf("%d %c\n", 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 26-05-2008


Introducción a analizadores léxicos 23
for(j=0; j<20;j++)
{
fread(&s, sizeof(s), 1, stream);
printf("%d %c\n", 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 26-05-2008


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, "%d\t", 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 26-05-2008


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. ............................................................................................................................................. 7
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 26-05-2008

También podría gustarte