Professional Documents
Culture Documents
Teoría de Lenguajes
(Unidad III)
Tema:
Descripción de lenguajes de programación
Dictado por:
DIANA CECILIA MUÑOZ CASANOVA
M.S. en Ingeniería de Sistemas e Informática
CHIMBOTE – PERÚ
2007
CAPÍTULO I: INTRODUCCIÓN A LOS COMPILADORES
1.1. INTRODUCCIÓN
En un mundo informatizado como en el que vivimos, en el que cada día que pasa
dependemos más y más de un sistema informático eficiente, el cual debe estar
preparado para brindarnos la más alta calidad de servicios y prestaciones. Además
de ser sencillo en su manejo y sobre todo confiable, siendo estas características
indispensables para el usuario final. Quien no se fija, ni pregunta como se realiza
determinada tarea, pero si es buen critico a la hora de ver resultados, pero hay
otros que contrarios a estos, se hace la pregunta del millón, "¿Cómo se logra hacer
tal y tal cosa? ,"¿Cómo es posible que un graficador trabaje tan rápido?, ¿Cómo es
posible que un procesador de palabra a la hora de usar un diccionario sea tan
eficiente?, ¿Cómo es posible llevar los resultados de una aplicación a otra?, o
¿Cómo es posible que un programa que fue creado por una empresa puede trabajar
con los datos de obtenidos de otro programa, echo por otra empresa?.
Muchas pueden ser las respuestas, algunos argumentaran que es el sistema
operativo, otros dirán que son las normas y estándares establecidos, otros dirán
irónicamente que es más sencillo de lo que se piensa, dirán que se hace clac con la
rata aquí, se arrastra y se lleva a donde se quiere. Todos ellos tienen razón, sin
embargo si indagamos más a fondo. Surgirán preguntas más directas como por
ejemplo "¿Cómo se logra tal velocidad, con tan buen manejo de gráfico?", claro
que a todas ellas se puede responder diciendo, que todo se logra gracias al
Hardware, y no estaríamos totalmente errados, porque un buen Hardware conlleva
a un buen resultado, a una buena calidad de impresión en caso de volcado al papel,
una buena imagen si hablamos de gráficos, o un buen tiempo de respuesta a la
hora de realizar algún calculo matemático, pero siempre el Hardware será una
parte, solo una parte.
Es en este punto donde surge el Software, los programas, o como el modismo
denota hoy en día, las aplicaciones. Es decir que para obtener un buen resultado no
solo hace falta un buen Hardware acorde a los requerimientos de la tarea que se
quiere realizar, sino que calidad, eficiencia, velocidad, y confiabilidad hoy en día
son sinónimos de un buen trabajo en conjunto de la dupla Hardware y Software.
Dejando de lado lo que es el Hardware, y profundizando lo que representa su par,
palabra ya adoptada en nuestro idioma, y muy usada en el mundo de la
informática, deducimos que para obtener un buen software, ante todo esta el
aspecto creador de quien lo realiza, luego hay que ver cual será el entorno de
trabajo en que actuara, cuales serán los requerimientos del mismo, hay que saber
elegir que paradigma de programación se usará.
Después de formuladas estas preguntas y de haber respondido a las mismas de
manera apropiada. Hay que elegir cual es el lenguaje de programación más
conveniente para realizar dicha tarea.
Un lenguaje de programación o más genéricamente de computación, es el medio
por el cual el hombre interactúa con un ordenador. Pero el lenguaje de
computación no es lo único que se necesita para que se produzca la comunicación,
hace falta otro componente importante para completar el medio de comunicación,
y es en este punto donde surge la palabra "interprete", cuya definición dice
"persona que explica a otra en lengua que entienda" en el ámbito informático,
olvidémonos por un instante de la palabra persona, e "interpretemos" su
significado. Se puede decir entonces que a través de un intérprete podríamos pedir
a un ordenador que realice una tarea determinada sin preocuparnos de los detalles.
Claro que este interprete no es más que una aplicación (programa), que realiza la
traducción de lo que pedimos que comunique a un ordenador, por lo tanto un
interprete es capaz de conocer dos lenguajes el del emisor y el del receptor.
En el mundo de la informática existen muchos lenguajes de programación, que
trabajan con uno o varios paradigmas de programación (estilos, formas, métodos
de programación), por lo tanto es de suponer que existen distintas reglas de
sintaxis y semántica para cada lenguaje de programación como lo existe en
cualquier otro lenguaje, sea cualquiera el tipo y contexto al que pertenezca. En
tiempos ya muy remotos según cuentan los que saben proliferaban los intérpretes,
quienes tomaban la petición del usuario (hombre) e "interpretaban" la misma y se
la comunicaban al ordenador quien la ejecutaba, y esperaba una nueva petición. La
petición era declarada dentro los parámetros de definición del lenguaje usado. Con
el tiempo se vio que el estar realizando una interpretación y traducción cada vez
que se necesita realizar algo era poco efectivo, en cuanto a tiempo de trabajo del
Hardware se refiere, y más aun si se trataba de un conjunto grande de
instrucciones (peticiones), es aquí donde entraron en juego los compiladores,
quien al igual que sus antecesores realizan una traducción de los programas
(conjunto de intrusiones de un lenguaje) revisando que este dentro del marco de
definición del lenguaje de programación utilizado. Con la diferencia que la
traducción se realiza una sola vez y de todo el programa.
Después de todos estos tópicos previos, podemos decir que la calidad de un buen
software es producto de un lenguaje de computación versátil, flexible y veloz,
todo sinónimo de buen compilador, claro sin dejar de lado la capacidad creadora
del programador (usuario, nosotros).
"¿Cómo funciona un compilador? ", es la pregunta de todo aquel que se hace
llamar programador, a grandes rasgos un compilador toma un programa escrito en
un lenguaje de programación con una gramática, sintaxis y semántica definida,
revisa que este dentro de las pautas de definición del lenguaje, y lo traduce en
programa de salida escrito en lenguaje binario, el cual es entendido por el
ordenador y por lo tanto puede ser ejecutado por el mismo (recordar que un
interprete a diferencia de un compilador no traduce todo el programa sino que va
realizando la traducción e interpretación de la misma paso a paso, sin crear ningún
programa de salida ejecutable). Las partes del proceso de compilación se dividen
en dos: una llamada fase de Análisis y otra llamada fase de Sintaxis, las cuales
interactúan entre si para la creación de la tabla de símbolos y el control del
manejador de errores, dentro del análisis hay tres etapas llamadas análisis
lexicográfico, análisis sintáctico, análisis semántico. Dentro de la fase de Síntesis
existen las etapas de generación de código intermedio, optimización de código
intermedio, y generación de código.
Al tener que describir como funciona un compilador tenemos que tener en claro en
no confundir los términos compilador y compilación, se debe entender que al decir
compilador nos referimos al programa, y al decir compilación al proceso en sí. En
esencia ambos términos cumplen con la definición más simple de un compilador,
es decir, sea el programa compilador o el proceso de compilación, ambos reciben
como entrada un código escrito en algún lenguaje y producen como salida otro
código escrito en otro lenguaje.
1.2. HISTORIA
Avances en Compilación
Junto a este desarrollo en los lenguajes (ALGOL), también se iba avanzando en la
técnica de compilación. En 1958 proponían una solución al problema de que un
compilador fuera utilizable por varias máquinas objeto. Para ello, se dividía por
primera vez el compilador en dos fases, designadas como el "front end" y el "back
end". La primera fase (front end) es la encargada de analizar el programa fuente y
la segunda fase (back end) es la encargada de generar código para la máquina
objeto. El puente de unión entre las dos fases era un lenguaje intermedio que se
designó con el nombre de UNCOL (UNiversal Computer Oriented Language).
Para que un compilador fuera utilizable por varias máquinas bastaba únicamente
modificar su back end. Aunque se hicieron varios intentos para definir el UNCOL,
el proyecto se ha quedado simplemente en un ejercicio teórico. De todas formas,
la división de un compilador en front end y back end fue un adelanto importante.
En los primeros lenguajes (FORTRAN y ALGOL 60) los tipos posibles de los
datos eran muy simples, y la comprobación de tipos era muy sencilla. No se
permitía la corrección de tipos, pues ésta era una cuestión difícil.
Con la aparición del ALGOL 68 se permitía que las expresiones de tipo fueran
construidas sistemáticamente. Más tarde, de ahí surgió la equivalencia de tipos por
nombre y estructural.
También se desarrollaron estrategias para mejorar las rutinas de entrada y de salida
de un procedimiento . Así mismo, y ya desde los años 60, se estudió el paso de
parámetros a un procedimiento por nombre, valor y variable.
Optimización en la Compilación
La técnica de la optimización apareció desde el desarrollo del primer compilador
de FORTRAN. Backus comenta cómo durante el desarrollo del FORTRAN se
tenía el miedo de que el programa resultante de la compilación fuera más lento
que si se hubiera escrito a mano. Para evitar esto, se introdujeron algunas
optimizaciones en el cálculo de los índices dentro de un bucle. Pronto se
sistematizan y se recoge la división de optimizaciones independientes de la
máquina y dependientes de la máquina.
Entre las primeras están la propagación de valores , el arreglo de expresiones,
la eliminación de redundancias, etc.
Entre las segundas se podría encontrar la localización de registros, el uso de
instrucciones propias de la máquina y el reordenamiento de código.
Compilación en la Actualidad
En la actualidad, el proceso de la compilación ya está muy asentado. Un
compilador es una herramienta bien conocida, dividida en diversas fases. Algunas
de estas fases se pueden generar automáticamente (analizador léxico y sintáctico)
y otras requieren una mayor atención por parte del escritor de compiladores (las
partes de traducción y generación de código).
De todas formas, y en contra de lo que quizá pueda pensarse, todavía se están
llevando a cabo varias vías de investigación en este fascinante campo de la
compilación:
Por una parte, se están mejorando las diversas herramientas disponibles (por
ejemplo, el generador de analizadores léxicos Aardvark para el lenguaje
PASCAL).
También la aparición de nuevas generaciones de lenguajes -ya se habla de la
quinta generación, como de un lenguaje cercano al de los humanos-ha
provocado la revisión y optimización de cada una de las fases del compilador.
Traductor.
Cualquier programa que toma como entrada un texto escrito en un lenguaje
llamado fuente y da como salida un programa equivalente en otro lenguaje, el
lenguaje objeto. Si el lenguaje fuente de un lenguaje de programación de alto
nivel y el objeto un lenguaje de bajo nivel (ensamblador o código de máquina),
al traductor se le denomina compilador.
Ensamblador.
Es un programa traductor cuyo lenguaje fuente es el lenguaje ensamblador.
Gráfico Nº 2: Ensambladores
Intérprete.
Es un programa que no genera un programa equivalente, sino que toma una
sentencia del programa fuente en un lenguaje de alto nivel y la traduce al
código equivalente y al mismo tiempo lo ejecuta.
En un principio debido a la escasez de memoria se utilizaban más los
intérpretes, ahora se usan más los compiladores (a excepción de JAVA)
Mensaje de error
Cualquier compilador debe realizar dos tareas principales: análisis del programa a
compilar y síntesis de un programa en lenguaje maquina que, cuando se ejecute,
realizara correctamente las actividades descritas en el programa fuente. Para el
estudio de un compilador, es necesario dividir su trabajo en fases. Cada fase
representa una transformación al código fuente para obtener el código objeto. La
siguiente figura representa los componentes en que se divide un compilador. Las
tres primeras fases realizan la tarea de análisis, y las demás la síntesis. En cada una
de las fases se utiliza un administrador de la tabla de símbolos y un manejador de
errores.
2.1.2.COMPILACIÓN
Recibe el código fuente puro, este es él modulo principal de un compilador,
pues si ocurriera algún error en esta etapa el compilador no podría avanzar. En
esta etapa se somete al código fuente puro de entrada a un análisis léxico
gráfico, a un análisis sintáctico, a un análisis semántico, que construyen la
tabla de símbolos, se genera un código intermedio al cual se optimiza para así
poder producir un código de salida generalmente en algún lenguaje
ensamblador.
2.1.3.ENSAMBLADO
Este modulo no es ni más mi menos que otro compilador pues recibe un
código fuente de entrada escrito en ensamblador, y produce otro código de
salida, llamado código binario no enlazado. Si por un momento viéramos a
este modulo como un programa independiente, veríamos que en este caso los
términos programa compilador y proceso de compilación son los mismos.
Pues este modulo no es mas que un compilador, que en su interior realiza
como su antecesor un análisis léxico gráfico, un análisis sintáctico, un análisis
semántico, crea una tabla de símbolos, genera un código intermedio lo
optimiza y produce un código de salida llamado código binario no enlazado, y
a todo este conjunto de tares se los denomina proceso de compilación. Como
se puede ver este compilador (llamado ensamblador) a diferencia de los demás
compiladores no realiza una expansión del código fuente original(código
fuente de entrada), tiene solamente un proceso de compilación y por supuesto
no enlaza el código fuente. Es un compilador que carece de los módulos de
preprocesado y enlazado, y donde los módulos de compilación y ensamblado
son los mismos.
2.1.4.ENLAZADO
El cuarto y ultimo modulo es el encargado de realizar el enlazado del código
de fuente de entrada (código maquina relocalizable) con las librerías que
necesita, como así también de proveer al código de las rutinas necesarias para
poder ejecutarse y cargarse a la hora de llamarlo para su ejecución, modifica
las direcciones relocalizables y ubica los datos en las posiciones apropiadas de
la memoria. Este ultimo modulo es el que produce como salida el código
binario enlazado. Ya sea dinámico o estático, al decir dinámico se refiere a que
el código producido utiliza librerías dinámicas (librerías ya cargadas en el
sistema), esto implica que se obtendrá un código más corto y que se
actualizara automáticamente si aparece alguna nueva versión de las librerías,
mientras que el estático se refiere al echo que no se realiza enlace con ninguna
librería y por lo tanto se obtendrá un código mas largo con una copia de las
rutinas de librería que necesita.
Biblioteca
(Archivos objeto relocalizables)
Fase de Análisis
Análisis Léxico
Análisis Sintáctico
Análisis Semántico
Fase de Síntesis
Etapa de Generación de Código Intermedio
Etapa de Optimización de Código
Etapa de Generación de Código
2.2.1.FASE DE ANÁLISIS
En esta fase se crea una representación intermedia de código
Análisis semántico.
La fase de análisis semántico se intenta detectar instrucciones que tengan
la estructura sintáctica correcta, pero que no tengan significado para la
operación implicada.
2.2.2.FASE DE SÍNTESIS
Genera un código a partir de la representación intermedia
Optimización de código.
En esta fase se trata de mejorar el código intermedio, de modo que resulte
un código de máquina más rápido de ejecutar.
Generación de código.
Esta constituye la fase final de un compilador. En ella se genera el código
objeto que por lo general consiste en código en lenguaje máquina (código
relocalizable) o código en lenguaje ensamblador.
Además existen:
Administrador de la tabla de símbolos.
Una tabla de símbolos es una estructura de datos que contiene un registro
por cada identificador. El registro incluye los campos para los atributos del
identificador. El administrador de la tabla de símbolos se encarga de
manejar los accesos a la tabla de símbolos, en cada una de las etapas de
compilación de un programa.
Manejador de errores.
En cada fase del proceso de compilación es posibles encontrar errores. Es
conveniente que el tratamiento de los errores se haga de manera
centralizada a través de un manejador de errores. De esta forma podrán
controlarse más eficientemente los errores encontrados en cada una de las
fases de la compilación de un programa.
2.3. EJEMPLO DEL PROCESO DE COMPILACIÓN
Análisis Léxico
El analizador léxico lee los caracteres del programa fuente, y verifica que
correspondan a una secuencia lógica (identificador, palabra reservada etc.). Esta
secuencia de caracteres recibe el nombre componente léxico o lexema. En este
caso el analizador léxico verifica si el identificador id1 (nombre interno para
"suma") encontrado se halla en la tabla de símbolos, si no esta produce un error
porque todavía no fue declarado, si la preposición hubiese sido la declaración del
identificador "suma" en lenguajes C, C++ (int suma;) el analizador léxico
agregaria un identificador en la tabla de símbolos, y así sucesivamente con todos
los componentes léxicos que aparezcan, los componentes léxicos resultantes de la
expresión son:
Identificador: suma.
El símbolo de asignación: =
Identificador: var1
Operador: +
Identificador: var2
Operador: +
Numero: 10
Que en el análisis léxico y con la tabla de símbolos es:
id1= id2+ id3 * 10
Análisis Sintáctico
El analizador sintáctico impone una estructura jerárquica a la cadena de
componentes léxicos, generada por el analizador léxico, que es representada en
forma de un árbol sintáctico.
=
/ \
id1 +
/ \
id2 +
/ \
id3 10
Análisis Semántico
El analizador semántico verificara en este caso que cada operador tenga los
operandos permitidos.
=
/ \
id1 +
/ \
id2 +
/ \
id3 tipo_ent
|
10
Generador de código
Finalmente lleva el código intermedio a un código objeto que en este caso es un
código relocalizable o código ensamblador (también llamado código no enlazado).
MOVF id3, R2
MULT #10.0, R2
MOVF id2, R1
ADDF R2, R1
MOVF R1, id1
# 1 "hmundo.c"
# 1 "c:/compilador/include/stdio.h" 1 3
# 1 " c:/compilador/include/sys/types.h" 1 3
# 12 " c:/compilador/include/stdio.h" 2 3
typedef void *va_list;
typedef long unsigned int size_t;
typedef struct {
int _cnt;
char *_ptr;
char *_base;
int _bufsiz;
int _flag;
int _file;
char *_name_to_remove;
} FILE;
typedef unsigned long fpos_t;
extern FILE __stdin, __stdout, __stderr;
void clearerr(FILE *_stream);
int fclose(FILE *_stream);
int feof(FILE *_stream);
int ferror(FILE *_stream);
int fflush(FILE *_stream);
int fgetc(FILE *_stream);
int fgetpos(FILE *_stream, fpos_t *_pos);
char * fgets(char *_s, int _n, FILE *_stream);
FILE * fopen(const char *_filename, const char *_mode);
int fprintf(FILE *_stream, const char *_format, ...);
int fputc(int _c, FILE *_stream);
int fputs(const char *_s, FILE *_stream);
size_t fread(void *_ptr, size_t _size, size_t _nelem, FILE
*_stream);
FILE * freopen(const char *_filename, const char *_mode, FILE
*_stream);
int fscanf(FILE *_stream, const char *_format, ...);
int fseek(FILE *_stream, long _offset, int _mode);
int fsetpos(FILE *_stream, const fpos_t *_pos);
long ftell(FILE *_stream);
size_t fwrite(const void *_ptr, size_t _size, size_t _nelem,
FILE *_stream);
int getc(FILE *_stream);
int getchar(void);
char * gets(char *_s);
void perror(const char *_s);
int printf(const char *_format, ...);
int putc(int _c, FILE *_stream);
int putchar(int _c);
int puts(const char *_s);
int remove(const char *_filename);
int rename(const char *_old, const char *_new);
void rewind(FILE *_stream);
int scanf(const char *_format, ...);
void setbuf(FILE *_stream, char *_buf);
int setvbuf(FILE *_stream, char *_buf, int _mode, size_t _size);
int sprintf(char *_s, const char *_format, ...);
int sscanf(const char *_s, const char *_format, ...);
FILE * tmpfile(void);
char * tmpnam(char *_s);
int ungetc(int _c, FILE *_stream);
int vfprintf(FILE *_stream, const char *_format, va_list _ap);
int vprintf(const char *_format, va_list _ap);
int vsprintf(char *_s, const char *_format, va_list _ap);
int fileno(FILE *_stream);
FILE * fdopen(int _fildes, const char *_type);
int pclose(FILE *_pf);
FILE * popen(const char *_command, const char *_mode);
extern FILE _stdprn, _stdaux;
void _stat_describe_lossage(FILE *_to_where);
int _doprnt(const char *_fmt, va_list _args, FILE *_f);
int _doscan(FILE *_f, const char *_fmt, void **_argp);
int _doscan_low(FILE *, int (*)(FILE *_get), int (*_unget)(int,
FILE *), const char *_fmt, void **_argp);
int fpurge(FILE *_f);
int getw(FILE *_f);
int mkstemp(char *_template);
char * mktemp(char *_template);
int putw(int _v, FILE *_f);
void setbuffer(FILE *_f, void *_buf, int _size);
void setlinebuf(FILE *_f);
char * tempnam(const char *_dir, const char *_prefix);
int _rename(const char *_old, const char *_new);
# 1 "hmundo.c" 2
void main()
{
char* frase= " Hola Mundo...!!!";
printf("%s", frase );
};
.file "hmundo.c"
compiler_compiled.:
___compiled_c:
.text
LC0:
.ascii " Hola Mundo...!!!\0"
LC1:
.ascii "%s\0"
.align 2
.globl _main
_main:
pushl %ebp
movl %esp,%ebp
subl $4,%esp
call ___main
movl $LC0,-4(%ebp)
movl -4(%ebp),%eax
pushl %eax
pushl $LC1
call _printf
addl $8,%esp
L1:
leave
ret
Este código será analizado por él modulo de ensamblado, que lo llevara a código
binario no enlazado, y lo enviara al modulo de enlazado. El código de salida
enviado al modulo de enlazado es el siguiente.
L�³Ú(7ô�.text�
@�Œ�Ì�� �
.............
.data�@�@�@�
.bss�@�@�€
Hola Mundo...!!!�%s�ヘ v�
U‰åƒìèÝÿÿÿÇEü�‹EüPh�
èÈÿÿÿƒÄ Éà ヘ v�.file�þÿ�
ghmundo.c����.
.............
_main����
___main��
_printf��%�
_compiled.�
___compiled_c�
Finalmente él modulo de enlazado, vincula el código binario sin enlazar con las
librerías dinámicas necesarias según sea el caso o no. Y produce como salida el
código binario enlazado o código binario ejecutable.
MZ�� �'�ÿÿ�`�T�
$Id: xxx.asm built mm/dd/aa 00:00:00 by ...asm $
@(#) xxx.asm built mm/dd/aa 00:00:00 by ...asm
.............
]ヘ v� Hola Mundo...!!!�%s�ヘ v�
U‰åƒìèý �ÇEüx"‹EüPhŠ"è, �ƒÄ Éà ヘ v�387
No �80387 detected.
Warning: Coprocessor not present and DPMI setup failed!
If application attempts floating operations system may hang!
¸'�Éà ヘ v�¸"�ÉÃCall frame traceback EIPs:
0x�
0x�
Alignment Check�Coprocessor Error�Page fault�General
Protection Fault�Stack
Fault�Segment Not Present�Invalid TSS�Coprocessor
overrun�Double
............
Division by Zero�: sel=� invalid� base=� limit=�
ヘ v�U‰åƒìS‹]jÿu jè?�
............
LEX y YACC
Herramientas que nos permiten desarrollar componentes o la mayor parte
de un compilador
Son un recurso invaluable para el profesional y el investigador
Existen paquetes freeware
El analizador léxico, también conocido como scanner, lee los caracteres uno a uno
desde la entrada y va formando grupos de caracteres con alguna relación entre sí
(tokens), que constituirán la entrada para la siguiente etapa del compilador. Cada
token representa una secuencia de caracteres que son tratados como una única
entidad. Por ejemplo, en Pascal un token es la palabra reservada BEGIN, en C:
WHILE, etc.
Las tiras específicas sólo tienen tipo (lo que representan), mientras que las tiras no
específicas tienen tipo y valor. Por ejemplo, si “Contador” es un identificador, el
tipo de token será identificador y su valor será la cadena “Contador”.
El Analizador Léxico es la etapa del compilador que va a permitir saber si es un
lenguaje de formato libre o no. Frecuentemente va unido al analizador sintáctico
en la misma pasada, funcionando entonces como una subrutina de este último. Ya
que es el que va leyendo los caracteres del programa, ignorará aquellos elementos
innecesarios para la siguiente fase, como los tabuladores, comentarios, espacios en
blanco, etc.
1. Tipos de tokens:
tiras específicas, tales como palabras reservadas (if, while, begin,
etc.), el punto y coma, la asignación, los operadores aritméticos o
lógicos, etc.
tiras no específicas, como identificadores, constantes o etiquetas.
Ejemplo:
3.7.3.LEXEMA
Es cada secuencia de caracteres concreta que encaja con un patrón, es decir, es
como una instancia de un patrón.
Ejm: 8, 23, 50 (son lexemas que encajan con el patrón ( 0 | 1 | 2 | ... | 9) + )
Una vez detectado que un grupo de caracteres coincide con un patrón, se ha
detectado un lexema. A continuación se le asocia un número, que se le pasará
al sintáctico, y, si es necesario, información adicional, como puede ser una
entrada en la tabla de símbolos.
La tabla de símbolos suelen ser listas encadenadas de registros con parte
variable: listas ordenadas, árboles binarios de búsqueda, tablas hash, etc.
Ejm: Hacer un analizador léxico que nos reconozca los números enteros, los
números reales y los identificadores de usuario. Vamos a hacer este ejemplo en
C.
Terminales Expresión Regular
( 0 ... 9) + NUM_ENT
(0 ... 9)*. (0 ... 9) + NUM_REAL
(a ... z) (a ... z 0 ... 9) * ID
En vez de trabajar con los números 280, 281, 282, se definen mnemotécnicos.
# define NUM_ENT 280
# define NUM_REAL 281
# define NUM_ID 282
(“ ”\t \n)
(0 ... 9) + {return NUM_ENT;}
(0 ... 9) *. (0 ... 9) + {return NUM_REAL;}
(a ... z) (a ... z 0 ... 9)* {return ID;}
Las palabras que entran por el patrón (“ ”\t \n) no tienen acción asociada, por
lo que , por defecto, se consideran solo espaciadores.
Ejemplo:
Programa UNO;
Inicio
Escribe (“Hola”);
Fin.
Cuya tabla de tokens es la siguiente:
Durante estas etapas se tiene comunicación con la tabla de símbolos que concentra
información de las entidades empleadas en el programa.
3.11.TRATAMIENTO DE LOS ERRORES
Son aquellas que los lenguajes de programación “reservan” para usos particulares.
¿Cómo diferenciarlas de los identificadores?
Sea r una expresión regular sobre ∑. El lenguaje descrito por r, L(r), se define
recursivamente de la siguiente forma:
Ejemplo:
L(a*(a+b)) = L(a*)L((a+b)) = L(a)*L(a+b) = L(a)*(L(a) ∪ L(b)) = {a}*({a}∪
{b})
= {λ, a, aa, aaa,...}{a,b}
= {a, aa, ..., b, ab, aab, ...} = {an| n ≥ 1}∪{ an b | n ≥ 0}.
L((aa)*(bb)*b)= {a2nb2m+1 | n,m ≥ 0}.
Si ∑ = {a,b,c},entonces L(( a + b + c) )= ∑*
L(a*(b + c))
L(0*10*)
3.18.TEOREMAS DE EQUIVALENCIA
Por tanto, deben existir algoritmos que permitan obtener un autómata o una
gramática regular a partir de una expresión regular y viceversa.
3.19.MATRICES DE TRANSICIÓN
Ejemplo:
Automata que acepta cadenas con un número par de ceros y un numero par de
unos:
AFD = {S, A, B, C}, {0,1},δ, Q, {Q}
Representación gráfica:
Ejemplo:
AFND que reconoce en {a, b, c}* tales que el ultimp símbolo en la caden de
entrada aparecía también anteriormente en la cadena. En este AFND, seria
F = {q0, q1, q2, q3, q4}, {a, ,b, c}, δ, q0, { q4}
4.1. GRAMÁTICAS
Ejemplo:
Supóngase que utilizamos E en lugar de <expresion> para la variable de la
gramática. Podemos expresar esta gramática de la manera formal como:
G = (N,T,P,S) Donde: T = { + , * , ( , ) , id }
N={E }
P={EE+E
EE*E
E(E)
E id }
S=E
G = (N, T, P, S)
N: Conjunto de Símbolos No Terminales
T: Conjunto de Símbolos Terminales
P: Conjunto de Reglas de Producción
S∈N: Símbolo Inicial
Las gramáticas regulares guardan estrecha relación con los autómatas finitos.
Las gramáticas estudiadas en la teoría de lenguajes son la 2 y 3.
4.2. GRAMÁTICAS LIBRES DE CONTEXTO Y ANÁLISIS SINTÁCTICO
En este tema veremos que una GLC escrita de esta forma más general, siempre
puede escribirse en la versión más restrictiva (sólo S puede producir ).
4.4. CONCEPTOS SOBRE GLCs
4.4.1.ÁRBOL DE DERIVACIÓN
Sea G=(N,T,P,S) una GLC. Un árbol es un árbol derivación para G si:
1. Todo vértice tiene una etiqueta tomada de T ∪ N ∪ {λ}
Ejemplo:
Sea G = {N, T, P, S} una GLC cobn P: S ab | aSb.
La derivación de la cadena aaabbb sera S ⇒ aSb ⇒ aaSbb ⇒ aaabbb y el
arbol de derivación:
4.4.2.GRAMÁTICAS NO AMBIGUAS
Sea G = ( T , N , P , S ) que acepta expresiones aritméticas como:
X+Y–X*Y
T={X,Y,+,-,*,/,(,)}
Arbol de derivacion:
4.4.3.GRAMÁTICAS AMBIGUAS
Sea G = ( T , N , P , S ) que acepta expresiones aritméticas como:
X+Y–X*Y
T={X,Y,+,-,*,/,(,)}
N = { EXPR , OP }
P = { EXPR EXPR OP EXPR | ( EXPR ) | X | Y
OP + | - | * | /
S = {EXPR}
G es ambigua, porque tiene más de un árbol de derivación
EXPR ( EXPR )
( EXPR OP EXPR )
( X OP EXPR )
( X + EXPR )
( X + ( EXPR ) )
( X + ( EXPR OP EXPR ) )
( X + ( Y OP EXPR ) )
( X + ( Y - EXPR ) )
( X + ( Y – ( EXPR OP EXPR ) ) )
( X + ( Y – ( X OP EXPR ) ) )
( X + ( Y – ( X * EXPR ) ) )
(X+(Y–(X*Y)))
Aquí podemos ver que esta regla maneja las distintas alternativas que puede
contener un dígito. El símbolo ' | ' significa ' o ' , ::= significa un implica, <dígito>
significa la lo que estamos definiendo ( identificador ) y dígito es una categoría
sintáctica o un no terminal . Con esto podemos empezar a definir algunas
estructuras como el IF.
< entero sin signo > ::= < dígito > | < entero sin signo >< dígito >
Con esto ,vemos que la notación BNF maneja una serie de reglas gramaticales que
empiezan desde a lo más pequeño hasta lo más grande.
Ahora bien, existen otras gramáticas que se basan en esta gramática BNF.
¿Cuando puede reducirse por una parte izquierda lo que parece ser la parte
derecha de una regla?
Puede haber partes derechas comunes
Puede haber producciones ε
Tipos de gramáticas
Gramáticas LR
Conjunto más amplio de gramáticas que LL(1)
Expresión más sencillas
LR(1) Analizadores LR(1)
LALR Analizadores LALR
SLR Analizadores SLR
Condiciones SLR
Analizadores reducción-desplazamiento
Analizadores por reducción – desplazamiento
• Analizador LR
• Analizador LALR
• Analizador SLR
El algoritmo de análisis es común
Términos comunes
o IrA (s, A)
Construcción
Propósito
Crear un analizador descendente O(n)
Debe decidir qué regla aplicar según token
La gramática debe ser LL(1)
L. Análisis de izquierda a derecha
L. Derivaciones por la izquierda
1. Un token permite decidir la regla de producción
Se elimina la recursividad
4.15.CONJUNTOS DE PREDICCIÓN
Construcción
Conjunto Primero PRIM (α)
Conjunto Siguiente SIG (A)
Regla
4.16.CONJUNTO PRIMERO
Construcción
4.17.CONJUNTO SIGUIENTE
4.19.ELIMINACIÓN DE LA RECURSIVIDAD
Tipos de recursividad
Directa. Una gramática G es recursiva si tiene alguna regla de producción
que sea recursiva por la izquierda
Eliminación de la recursividad
Eliminación de la recursividad
http://html.rincondelvago.com/lenguajes-de-programacion_historia-y-
evolucion.html
http://www.desarrolloweb.com/articulos/2358.php
http://www.monografias.com/trabajos16/lenguaje-miranda/lenguaje-miranda.shtml
http://enciclopedia.us.es/index.php/Lenguaje_imperativo
http://www.monografias.com/trabajos/tendprog/tendprog.shtml
http://www.monografias.com/trabajos/lengprog/lengprog.shtml
http://www.zonatenisatp.com/index.php
http://www.zonatenisatp.com/tema1_1_01_abstraccion.php
http://www.zonatenisatp.com/tema1_2_00_abstraccion_datos.php
http://www.zonatenisatp.com/tema1_2_03_lenguajes_programacion.php
http://ultimaorbita.com/wiki//index.php?title=Abstraccion_de_datos_y_abstraccion_
de_control._Evolucion_segun_los_paradigmas
-http://yalma.fime.uanl.mx/~elisa/teaching/prog/herencia.pdf
http://ib.cnea.gov.ar/CursoOO/tipos.htm
http://es.wikipedia.org/wiki/Encapsulamiento_(programaci%C3%B3n_orientada_a_
objetos)
http://www.cs.uu.nl/~jeroen/courses/fp-sp.pdf
http://www.inf.unitru.edu.pe/~pelm/Modelos/Funcion.html
http://www.dsic.upv.es/users/elp/temas/ProgFunc.html
http://juanfc.lcc.uma.es/EDU/EP/trabajos/T201.Clasificaciondelostiposdelenguajes.
pdf
http://www.esimez.ipn.mx/acadcompu/apuntes_notas%20breves/programacion_orie
ntada_objetos.pdf
http://www.monografias.com/trabajos20/paradigmas-de-programacion/paradigmas-
de-programacion.shtml
http://static.scribd.com/docs/7a6vhoquhqs22.pdf
http://www.scribd.com/doc/9762/Programacion-Orientada-a-Objetos
http://platea.cnice.mecd.es/~jmarti2/materiales/resumenLePr.pdf
http://horustealth.tripod.com/pascal.htm
http://decsai.ugr.es/~dpelta/ProgOrdenadores/tema5.pdf
http://ar.geocities.com/luis_pirir/cursos/procedimiento.htm
http://www.inf.udec.cl/~mvaras/estprog/cap41.html
http://www.monografias.com/trabajos/objetos/objetos.shtml
http://www.gnacademy.org/text/cc/Tutorial/Spanish/node5.html
http://www.gnacademy.org/text/cc/Tutorial/Spanish/node6.html
-http://www.desarrolloweb.com/articulos/2358.php
http://www.desarrolloweb.com/articulos/2387.php
http://es.wikipedia.org/wiki/Compilador
http://www.investigacion.frc.utn.edu.ar/labsis/Publicaciones/InvesDes/Compiladore
s/rxc.htm
http://arantxa.ii.uam.es/~alfonsec/docs/compila1.htm
http://www.monografias.com/trabajos11/compil/compil.shtml
http://mx.geocities.com/alfonsoaraujocardenas/compiladores.html
http://platon.escet.urjc.es/grupo/docencia/automatas/
http://kataix.umag.cl/~jaguila/Iec/Compiladores/Automatas/ta_cap1_2.html
http://arantxa.ii.uam.es/~alfonsec/docs/compila4.htm