Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Apéndice 3
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
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.
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.
S::=aA
A::=b|cA
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
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.
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
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.
Símbolo terminal.
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.
Alternativa.
La producción: A::=B1|B2|…|Bn
Se representa:
B1
A
B2
Bn
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.
b1 B1
A
b2 B2
bn Bn
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:
A
B1 B2 Bn
Figura A3.6. Concatenación.
Repetición.
La producción: A::={B}
Se representa:
En el reconocedor, se implementa:
while( esta_en(L, ch) ) B( );
B 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.
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.
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 +
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:
{ „+„ } { „)‟ } =
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 ch='\0';
fclose(stream);
return 0;
}
int main(void)
{
parser();
return 0;
}
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>
char simbolo='\0';
int nl=1; //contador de líneas
int nc=0; //contador de caracteres en la línea.
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
}
int main(void)
{
bnfparser();
return 0;
}
A = C.
B=x,A.
B=x,A,B,C-
C=x(B,D.
D=(A).
*
C=x(B,D.
(4,8): Esperaba cierre paréntesis
D=(A).
*fin de archivo número de líneas =5
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;
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.
Si es letra
Si es espacio
Si es alfanumérico
0 1
No es letra
Si no es alfanumérico
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
alfanum
6 5 isspace
!alfanum
alfanum
!alfanum
es alfanumérico id ==”define”
3 4
2
alfanum
Si es letra Si es alfanumérico
Si es espacio
0 1
No es letra ni #
Si no es alfanumérico
int main(void)
{ makenull();
procesa_archivos();
return 0;
}
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.
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.
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
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 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>.
Una vez completada la escritura de todas las líneas, se cierra el archivo, mediante:
fclose(stream);
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.
#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;
Si se intenta leer más allá del fin de archivo la función feof, retorna verdadero.
#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
}
for(i=0;i< ITEMS;i++)
{
printf("%d %c\n", arr[i].i, arr[i].ch);// muestra el arreglo
}
return 0;
}
#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 */
}
El archivo TEST.bin, no puede ser visualizado con un editor de texto. Para su interpretación
debe usarse un editor binario.
Para programas sencillos, como los ilustrados, puede generarse el ejecutable en ambiente UNIX,
mediante el comando: make <nombre de archivo c, sin extensión>
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))
{
int escribe_archivo(void)
{
FILE *stream;
Referencias.
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.