INTRODUCCION - Biblioteca Central de la Universidad Nacional

Anuncio
Universidad Nacional del Santa
Curso: Teoría de Compiladores
INTRODUCCION
La computadora es la máquina más versátil concebida por el hombre. Aunque inicialmente es
construida como un mecanismo de cálculo de prestaciones superiores a los primeros dispositivos mecánicos
y electromecánicos, con el paso del tiempo se le incorporaron capacidades para la realización de operaciones
lógicas y para la manipulación de datos no numéricos. Un largo proceso evolutivo ha llevado a este
dispositivo calculador de la aritmética de cifras a la generación de información, teniendo como meta
inmediata la manipulación del conocimiento y como objetivo futuro la generación de conciencia.
Las actuales computadoras son capaces de realizar los más complejos cálculos aritméticos, lógicos y
simbólicos, de emular los más elaborados mecanismos (incluyendo a otros computadores), de simular
eventos naturales y de crear mundos virtuales. Las crecientes capacidades con que se presentan generación
tras generación acercan a los nuevos computadores cada vez más a la realización de tareas mucho más
complejas y que se antojan imposibles, como pueden ser la emulación de la mente y el pensamiento.
Todo esto se sustenta, por supuesto, en la electrónica y en la capacidad de programación del computador.
Sabemos que la electrónica del computador se denomina digital y que funciona con base en valores
discretos. Sabemos que es a través de códigos y estrategias de representación de datos como podemos
alimentar al computador con nuestras ideas y las expresiones con que identificamos a los objetos en nuestro
mundo análogo y tridimensional. También sabemos que mediante reglas de operación perfectamente
definidas podemos instruir al computador en la manipulación de dichos datos que llevarán a la generación
de otros y a la obtención de información en la resolución de problemas. La forma de expresar dicha
instrucción y la manera de llevar a cabo su traducción a lo que el computador es realmente capaz de
procesar son el tema de este curso.
El tema del diseño de compiladores es usualmente visto como uno de los más complejos, áridos y
abstractos. Adicionalmente suele considerarse que estos temas no dejan de ser de un interés meramente
académico, a menos que se trate de una enorme compañía de software dedicada a la creación de
herramientas de desarrollo. Tales creencias han dado lugar a una enorme variedad de mitos, algunos de
estos por ejemplo son:

"Para competir con productos de calidad en un mercado tan dinámico, amplio e internacional como
es el de la informática y computación, se requiere de una enorme cantidad de recursos humanos,
materiales y económicos. El desarrollo de herramientas de programación como intérpretes y
compiladores, ya sea para uso propio o comercialización, está reservado para las empresas que
disponen de dichos recursos. La mediana y pequeña empresa no cuenta con los recursos necesarios
para desarrollos internos de esta naturaleza, además de que no los necesita; si se trata de empresas
dedicadas al desarrollo de programas su mercado o está únicamente en el campo de la consultoría y
el desarrollo de sistemas a la medida o programación por contrato."

"El conocimiento teórico asociado con el diseño y creación de compiladores y lenguajes de
programación no son necesarios para un gerente de sistemas o líder de proyecto, esto es sólo para
el académico, lo importante es estar al día en lo que a tendencias y productos de hardware y
software se refiere."
Docente: Ing. Mirko Manrique Ronceros
~1~
Universidad Nacional del Santa

Curso: Teoría de Compiladores
"Las actividades en informática y computación en nuestro país están dedicadas al desarrollo de
software administrativo en su mayoría. Es poco probable que los profesionistas en estas áreas se
enfrenten al reto del desarrollo de un compilador o de un lenguaje de programación."

"Es más barato comprar software que desarrollarlo. Desarrollar software es lento y caro, es más
rápido comprar algo hecho."

"El diseño de compiladores y lenguajes de programación es una actividad reservada para
investigadores y catedráticos en ciencias computacionales."

"Siempre hay que usar un compilador, el código fuente queda seguro y la ejecución es más rápida.
Los intérpretes no son adecuados para el desarrollo de sistemas de información, estos son sólo un
recurso para la programación de computadoras pequeñas y para el usuario final."

"Los compiladores son hechos por nerds o gurús de la computación que dominan obscuros
lenguajes de programación, programan en lenguaje máquina y tienen un profundo conocimiento de
la arquitectura del computador."
Aunque al final del curso se darán cuenta el porqué las aseveraciones anteriores deben ser consideradas
como falsas o imprecisas.
Lenguaje
C++,
Fortran,
COBOL,
Pascal
Java
Modelo
Compilado
AWK, Basic,
SQL, Lisp,
Forth
Interpretado
Pseudo
Compilado
Características
Sintaxis específica para tipos de datos. Ideal para el desarrollo de
programas veloces o de tamaño reducido. Permiten la explotación de
instrucciones especiales del microprocesador. Mayor seguridad para evitar
alteración o robo de código fuente.
Transportabilidad absoluta. Requiere de una máquina virtual para ser
ejecutado. Mejor desempeño que un programa interpretado pero más
lento que uno compilado. Lenguajes de sintaxis rigurosa.
Requiere del intérprete para su ejecución. Desempeño lento. Ideal para
desarrollos rápidos (prototipos), operaciones no planeadas y programas
pequeños y simples. Lenguajes de sintaxis más relajadas y mayor libertad
para la conversión de datos.
Con el paso del tiempo los diversos lenguajes de programación han madurado. Hoy en día es posible
categorizarlos por las características que han venido exhibiendo con dicha maduración. De manera que
tenemos:

1GL o lenguajes de primera generación.- Esta fue (y continua siendo) aquella a la que pertenece el
Lenguaje Máquina, el nivel en el que datos instrucciones son dados como una serie de códigos
(binarios, octales, decimales o hexadecimales).

2GL o lenguajes de segunda generación.- Todos aquellos lenguajes ensambladores.

3GL o lenguajes de tercera generación.- También conocidos como lenguajes de nivel alto.

4GL o lenguajes de cuarta generación.- Generalmente un lenguaje 4GL es un lenguaje de propósito
específico, que proveen un lenguaje muy cercano al lenguaje natural o simbólico manejado en un
ámbito específico. Muchos lenguages son llamados 4GL cuando en realidad sólo son una mezcla de
3GL y 4GL o 3GL con extensiones de de dominio especícifico. Por ejemplo, el comando list en dBASE
es un comando propio de un 4GL pero las aplicaciones programadas en dBASE son 3GL. El siguiente
Docente: Ing. Mirko Manrique Ronceros
~2~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
ejemplo ilustra la diferencia de una sintaxis 3GL y 4GL para abrir un registro de clientes y mostrar su
contenido en pantalla.

5GL o lenguajes de quinta generación.- Estos lenguajes comienzan a ser identificados como
aquellos que hacen uso de los ambientes gráficos para llevar a cabo la programación del
computador a través de iconos o elementos gráficos similares.
COMPILADORES Y PROGRAMAS RELACIONADOS: DEFINICIONES Y CONCEPTOS
Aunque es equivocado, es común encontrar referencias en documentación de productos, publicidad
y textos (e inclusive escuchar a la gente del medio informático) utilizando los términos traductor,
compilador e intérprete de una forma libre e indistinta. Estas palabras no se utilizan para identificar de
manera genérica a un programa que nos permitiría poder programar una computadora. Debemos ser
precisos al emplear estas palabras, ya que se refieren a programas de distinta naturaleza que realizan
labores encaminadas a un objetivo específico y particular. Aunque la conducta manifestada pueda ser
similar, su comportamiento interno definitivamente es diferente.
Genéricamente hablando, en ciencias de la computación, los procesadores de lenguajes son aquellos
programas destinados a trabajar sobre una entrada que, por la forma como ha sido elaborada, pertenece a
un lenguaje en particular reconocido o aceptado por el programa en cuestión. Los procesadores de
lenguajes se clasifican como traductores o intérpretes.
TRADUCTOR
Un traductor es un programa que recibe una entrada escrita en un lenguaje (el lenguaje fuente) a una salida
perteneciente a otro lenguaje (el lenguaje objeto), conservando su significado. En términos computacionales
esto significa que tanto la entrada como la salida sean capaces de producir los mismos resultados.
INTERPRETE
Un intérprete, por otra parte, no lleva a cabo tal transformación; en su lugar obtiene los resultados conforme
va analizando la entrada.
Los intérpretes son útiles para el desarrollo de prototipos y pequeños programas para labores no previstas.
Presentan la facilidad de probar el código casi de manera inmediata, sin tener que recurrir a la declaración
previa de secciones de datos o código, y poder hallar errores de programación rápidamente. Resultan
inadecuados para el desarrollo de complejos o grandes sistemas de información por ser más lentos en su
ejecución.
Los traductores son clasificados en compiladores, ensambladores y preprocesadores.
Compiladores
Un compilador es un programa que recibe como entrada un programa escrito en un lenguaje de nivel medio
o superior (el programa fuente) y lo transforma a su equivalente en lenguaje ensamblador (el programa
objeto), e inclusive hasta lenguaje máquina (el programa ejecutable) pero sin ejecutarlo. Un compilador es
un traductor. La forma de como llevará a cabo tal traducción es el objetivo central en el diseño de un
compilador.
Docente: Ing. Mirko Manrique Ronceros
~3~
Universidad Nacional del Santa
Programa
Fuente
Curso: Teoría de Compiladores
Compilado
r
Programa
Objeto
Un compilador es un programa muy complejo con un número de líneas de código que puede variar de
10,000 a 1,000.000. Escribir un programa de esta naturaleza o incluso comprenderlo, no es tarea fácil, y la
mayoría de los científicos y profesionales de la computación nunca escribirán un compilador completo.
Ensamblador
Un ensamblador es el programa encargado de llevar a cabo un proceso denominado de ensamble o
ensamblado. Este proceso consiste en que, a partir de un programa escrito en lenguaje ensamblador, se
produzca el correspondiente programa en lenguaje máquina (sin ejecutarlo), realizando:

La integración de los diversos módulos que conforman al programa.

La resolución de las direcciones de memoria designadas en el área de datos para el almacenamiento
de variables, constantes y estructuras complejas; así como la determinación del tamaño de éstas.

La identificación de las direcciones de memoria en la sección de código correspondientes a los
puntos de entrada en saltos condicionales e incondicionales junto con los puntos de arranque de las
subrutinas.

La resolución de los diversos llamados a los servicios o rutinas del sistema operativo, código
dinámico y bibliotecas de tiempo de ejecución.

La especificación de la cantidad de memoria destinada para las áreas de datos, código, pila y
montículo necesarios y otorgados para su ejecución.

La incorporación de datos y código necesarios para la carga del programa y su ejecución.
Precompilador
Un precompilador, también llamado preprocesador, es un programa que se ejecuta antes de invocar al
compilador. Este programa es utilizado cuando el programa fuente, escrito en el lenguaje que el compilador
es capaz de reconocer (de aquí en adelante denominado lenguaje anfitrión-- en inglés host language),
incluye estructuras, instrucciones o declaraciones escritas en otro lenguaje (el lenguaje empotrado-- en
inglés embeded language). El lenguaje empotrado es siempre un lenguaje de nivel superior o especializado
(e.g. de consulta, de cuarta generación, simulación, cálculo numérico o estadístico, etcétera). Siendo que el
único lenguaje que el compilador puede trabajar es áquel para el cual ha sido escrito, todas las instrucciones
del lenguaje empotrado deben ser traducidas a instrucciones del lenguaje anfitrión para que puedan ser
compiladas. Así pu es un precompilador también es un traductor.
Los precompiladores son una solución rápida y barata a la necesidad de llevar las instrucciones de nuevos
paradigmas de programación (e.g. los lenguajes de cuarta generación), extensiones a lenguajes ya
existentes (como el caso de C y C++) y soluciones de nivel conceptual superior (por ejemplo paquetes de
simulación o cálculo numérico) a código máquina utilizando la tecnología existente, probada, optimizada y
confiable (lo que evita el desarrollo de nuevos compiladores). Facilitan la incorporación de las nuevas
herramientas de desarrollo en sistemas ya elaborados (por ejemplo, la consulta a bases de datos
relacionales substituyendo las instrucciones de acceso a archivos por consultas en SQL).
Docente: Ing. Mirko Manrique Ronceros
~4~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Resulta común encontrar que el flujo de proceso en los lenguajes de cuarta generación o de propósito
especial puede resultar demasiado inflexible para su implantación en los procesos de una empresa, flujos de
negocio o interacción con otros elementos de software y hardware, de aquí que se recurra o prefiera la
creación de sistemas híbridos soportados en programas elaborados en lenguajes de tercera generación con
instrucciones empotradas de nivel superior o propósito especial.
Pseudocompilador
Un pseudocompilador es un programa que actúa como un compilador, salvo que su producto no es
ejecutable en ninguna máquina real sino en una máquina virtual. Un pseudocompilador toma de entrada un
programa escrito en un lenguaje determinado y lo transforma a una codificación especial llamada código de
byte. Este código no tendría nada de especial o diferente al código máquina de cualquier microprocesador
salvo por el hecho de ser el código máquina de un microprocesador ficticio. Tal procesador no existe, en su
lugar existe un programa que emula a dicho procesador, de aquí el nombre de máquina virtual.
La ventaja de los pseudocompiladores que permite tener tantos emuladores como microprocesadores reales
existan, pero sólo se requiere un compilador para producir código que se ejecutará en todos estos
emuladores. Este método es una de las respuestas más aceptadas para el problema del tan ansiado
lenguaje universal o código portable independiente de plataforma.
Un intérprete es un programa que ejecuta cada una de las instrucciones y declaraciones que encuentra
conforme va analizando el programa que le ha sido dado de entrada (sin producir un programa objeto o
ejecutable). La ejecución consiste en llamar a rutinas ya escritas en código máquina cuyos resultados u
operaciones están asociados de manera unívoca al significado de la instrucciones o declaraciones
identificadas.
Ligadores
Tanto los compiladores como los ensambladores a menudo dependen de un programa conocido como
ligador, el cual recopila el código que se compila o ensambla por separado en diferentes archivos objetos, a
un archivo que es directamente ejecutable. En este sentido, puede hacerse una distinción entre código
objeto (código máquina que todavía no se ha ligado) y código de máquina ejecutable. Un ligador también
conecta un programa objeto con el código de funciones de librerías estándar, así como con recursos
suministrados por el sistema operativo de la computadora, tales como asignadotes de memoria y
dispositivos de entrada y salida. Es interesante advertir que los ligadores ahora realizan la tarea que
originalmente era una de las principales actividades de un compilador (de aquí el uso de la palabra
compilador: construir mediante la recopilación o compilación de fuentes diferentes).
Cargadores
Con frecuencia un compilador, ensamblador o ligador producirá un código que todavía no está
completamente organizado y listo para ejecutarse, pero cuyas principales referencias de memoria se hacen
relativas a una localidad de arranque indeterminada que puede estas en cualquier sitio de la memoria. Se
dice que tal código es relocalizable y un cargador resolverá todas las direcciones relocalizables relativas a
una dirección base, o de inicio, dada.
Docente: Ing. Mirko Manrique Ronceros
~5~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
El uso de un cargador hace mas flexible el código ejecutable, pero el proceso de carga con frecuencia ocurre
en segundo plano (como parte del entorno operacional) o conjuntamente con el ligado. Rara vez un
cargador es en realidad un programa por separado.
Editores
Los compiladores por lo regular aceptan programas fuente escritos utilizando cualquier editor que pueda
producir un archivo estándar, tal como un archivo ASCII. Más recientemente, los compiladores han sido
integrados junto con los editores y otros programas en un ambiente de desarrollo interactivo o IDE. En un
caso así, un editor, mientras que aún produce archivos estándar, puede ser orientado hacia el formato o
estructura del lenguaje de programación en cuestión. Tales editores se denominaban basados en estructura
y ya incluyen algunas de las operaciones de un compilador, de manera que, por ejemplo, pueda informarse
al programador de los errores a medida que el programa se vaya escribiendo en lugar de hacerlo cuando
está compilado. El compilador y sus programas acompañantes también pueden llamarse desde el editor, de
modo que el programador pueda ejecutar el programa sin tener que abandonar el editor.
Depuradores
Un depurador es un programa que puede utilizarse para determinar los errores de ejecución en un programa
compilado. A menudo está integrado en un IDE. La ejecución de un programa con un depurador se
diferencia de la ejecución directa en que el depurador se mantiene al tanto de la mayoría o la totalidad de la
información sobre el código fuente, tal como los números de línea y los nombres de las variables y
procedimientos. También
puede
detener la
ejecución en ubicaciones previamente
especificadas
denominadas puntos de ruptura, además de proporcionar información de cuáles funciones se ha invocado y
cuáles son los valores actuales de las variables. Para efectuar estas funciones el compilador debe suministrar
al depurador la información simbólica apropiada, lo cual en ocasiones puede ser difícil, en especial en un
compilador que intente optimizar el código objeto. De este modo, la depuración se convierte en una cuestión
de compilación.
Perfiladores
Es un programa que recolecta estadísticas sobre el comportamiento de un programa objeto durante la
ejecución. Las estadísticas típicas que pueden ser de interés para el programador son el número de veces
que se llama cada procedimiento y el porcentaje de tiempo de ejecución que se ocupa cada uno de ellos.
Tales estadísticas pueden ser muy útiles para ayudar al programador a mejorar la velocidad de ejecución del
programa. A veces el compilador utilizará incluso la salida del perfilador para mejorar de manera automática
el código objeto sin la intervención del programador.
PROCESO DE COMPILACION
Docente: Ing. Mirko Manrique Ronceros
~6~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Un compilador se compone internamente de varias etapas, o fases que realizan distintas operaciones
lógicas. Es útil pensar en estas fases como piezas separadas dentro del compilador, y pueden en realidad
escribirse como operaciones codificadas separadamente aunque en la práctica a menudo se integren juntas.
Figura 1.- Etapas del proceso de compilación.
Docente: Ing. Mirko Manrique Ronceros
~7~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
La entrada a este proceso es por supuesto el programa fuente. Por lo general éste es un archivo que es
creado por el usuario como un texto ASCII con o sin un formato específico aunque también puede ser el
resultado de algún otro proceso. A partir de este archivo diversos pasos pueden ser llevados a cabo:

Preprocesamiento.- Un preprocesador es la estrategia generalmente adoptada como solución a
lenguajes huéspedes, extensiones, lenguajes 4GL, o lenguajes de dominio específico. El
preprocesador es un traductor encargado de transformar dichas instrucciones a instrucciones del
lenguaje anfitrión (generalmente un tradicional 3GL) sobre las cuales finalmente trabajará el
compilador. Esta etapa es definitivamente opcional.

Análisis Léxico.- En esta fase, la cadena de caracteres que conforma al programa fuente es
despojada de comentarios, espacios en blanco y otros elementos superfluos. El programa encargado
de hacer esto es conocido como un scanner, y de aquí que al proceso se le refiera comúnmente
como scanning (exploración). Durante esta fase se identifican los elementos gramaticales usados en
la creación del programa. Cada elemento identificado es substituido por un código numérico
conocido como token.

Análisis Sintáctico.- La cadena de tokens resultante es alimentada a un programa conocido como
parser. El parser es el encargado de verificar que la secuencia y disposición de los tokens
corresponda con la sintaxis del lenguaje. Este proceso de verificación sintáctica es conocido como
parsing y es completamente guiado por la gramática del lenguaje.

Análisis Semántico y Generación de Código.- Una vez que la secuencia de tokens ha sido
validada, ésta es utilizada para identificar el sentido de la acción a realizar y generar el
correspondiente código en lenguaje máquina. Algunos compiladores recurren a la creación de código
intermedio para posteriormente generar la secuencia de instrucciones máquinas necesarias,
mientras que algunos otros proceden a la generación directa del código máquina.

Optimización de Código.- Esta es otra etapa opcional. La optimización de código es una actividad
que raya en un arte dominado solamente por un experimentado programador de ensamblador y
conocedor de la arquitectura del computador. Existen algunas técnicas desarrolladas al respecto
pero nada supera a la experiencia de un hábil programador. En esta etapa, ya sea posteriormente o
trabajando al unísono con el generador de código, secuencias de instrucciones y estructuras de
datos son examinadas buscando su substitución con secuencias, instrucciones o estructuras más
cortas, rápidas o eficientes.

Ligado.- Como paso final, todas las referencias pendientes de resolver sobre rutinas, módulos,
bibliotecas y dem´s porciones de código necesarias para el funcionamiento del programa son
cubiertas en esta parte. La resolución puede consistir desde el proporcionar meramente una
dirección o llamado a una función hasta la inclusión de enormes porciones de código.
Al final, como producto de todo este proceso, lo que se obtiene es un programa escrito en código máquina
que puede ser cargado en memoria y ejecutado. El proceso seguido por un intérprete es ligeramente
diferente, ya que mientras que cubre todas las etapas de análisis no cuenta con una fase síntesis. Un
intérprete no genera código, se limita a invocar rutinas ya escritas (proceso muchas veces llamado de
interpretación). La siguiente figura ilustra esto.
Docente: Ing. Mirko Manrique Ronceros
~8~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Figura 2.- Etapas del proceso de interpretación
En el caso de un pseudo-compilador, cuyo caso mejor conocido es el de Java, la diferencia
consiste en el código generado. Mientras que todas las etapas de un compilador son cubiertas, el
programa ejecutable no es creado para ser ejecutado en un procesador "real" sino para uno
"hipotético" o "imaginario" y conocido generalmente como máquina virtual. La máquina virtual es
otro programa cuyo funcionamiento simula al de un procesador. Este procesador recibe de entrada
el pseudo-código creado por el compilador y procede a la ejecución de las instrucciones contenidas
en éste; puede verse que no se trata más que de un intérprete muy sencillo.
Figura 3.- Etapas del proceso de pseudo-compilación.
Docente: Ing. Mirko Manrique Ronceros
~9~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
La siguiente figura ilustra con mayor detalle lo que pasa en cada una de las etapas del proceso de
compilación. El procesamiento de instrucciones de un lenguaje huesped (como puede ser SQL) correría a
cargo del pre-procesador, siendo transformadas instrucciones del lenguaje anfitrión. Durante la fase de
análisis léxico el scanner se encarga de identificar cada uno de los elementos usados para escribir el
programa fuente, substituyendo a cada uno de estos por un código numérico único (tokens). En este
proceso se eliminan comentarios y espacios en blanco. Los tokens son alimentados al analizador sintáctico
que valida que su disposición está acorde a las reglas del lenguaje. Validado este el analizador semántico
procede a identificar el propósito de las diversas secuencias de tokens y buscará generar representaciones
intermedias de cada acción o directamente código máquina. Este posteriormente es optimizado.
Figura 4.- Detalle del flujo de datos y acciones en el proceso de compilación.
Docente: Ing. Mirko Manrique Ronceros
~ 10 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
AMBIENTES DE COMPILACION
Los compiladores a menudo producen como resultado del análisis semántico, una forma de
representación intermedia del código fuente. Hoy en día, es cada vez más común que, en
ambientes de estación de trabajo o de computador central, todos los compiladores de los distintos
lenguajes generen el mismo código intermedio, el cual después, por un generador de código, es
transformado en el código objeto.
Esto tiene una gran ventaja: si se cambia el sistema operativo o alguna otra cosa, solo hay que
reemplazar el generador de código, y no todo todos los compiladores. La generación de códigos
intermedios aumenta la transportabilidad de los compiladores, ya que no es necesario cambiar sus
partes independientes de la máquina para un nuevo hardware distinto.
CODIGO FUENTE
CODIGO INTERMEDIO
CODIGO HEXADECIMAL
Int suma_enteros( int i,j, suma)
*SECTION 9 Define la sección de código.
00000000 222F 0004
{
* SECTION 14 define la sección de Pila
00000004 202F 0008
SECTION 9
00000008 206F 000C
xDEF .suma_enteros
0000000C D081
MOVE.L 4(A7), D1
0000000E 2080
MOVE.L 8(A7), D0
00000010 4E75
suma=i+j;
}
MOVE.L 12(A7).A0
ADD.L D1,D0
MOVE.L D0(A0)
RTS
* SECTION 14
* Asignaciones de suma_enteros
* 4(A7)
.i
* 8(A7)
.j
* 12(a7) .suma
ANALISIS Y SINTESIS
La compilación de un programa consiste en analizar y sintetizar dicho programa, es decir,
determinar la estructura y el significado de un código fuente y traducir ese código fuente a un
código de máquina equivalente.
Las tareas o fases principales de un compilador son:

Análisis léxico.

Análisis sintáctico.

Análisis semántico.

Generación de código.
Podemos considerar a un programa como un flujo de caracteres que sirven como entrada para el
análisis léxico. La tarea del análisis léxico consiste en reconocer los componentes léxicos dentro de
ese flujo, es decir, transformar un flujo de caracteres en un flujo de componentes léxicos (como
Docente: Ing. Mirko Manrique Ronceros
~ 11 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
los textos en lenguaje natural, podemos distinguir entre palabras y componentes léxicos: el
número de palabras determina el tamaño del vocabulario del programa, mientras que el número
de componentes léxicos determina la longitud del programa.
Por ejemplo: La proposición i:= 10;
Producirá lo siguiente:

el identificador i

el símbolo de asignación :=

el número 10

el símbolo delimitador; (punto y coma)
Los identificadores o nombres reconocidos se organizan en una tabla de símbolos, que es una
estructura de datos que contiene registros con campos de atributo para cada nombre.
El contenido de la tabla de símbolos se completa con el análisis léxico y sintáctico y se usará para
el análisis semántico y la generación de código.
El siguiente paso es el análisis sintáctico. La palabra “Sintaxis” significa “estructura del orden de
las palabras en una frase”. Otro término utilizado para el análisis sintáctico es el análisis
jerárquico. La tarea del análisis sintáctico es revisar si los símbolos aparecen en el orden correcto
(es decir, revisar si el programa fuente fue diseñado de acuerdo con la sintaxis del lenguaje de
programación) y combina los símbolos del código fuente para formar unidades gramaticales.
En esta fase se detectan errores de sintaxis como:
h + x := x * y
En general, las unidades gramaticales se organizan y representan con árboles
de análisis
sintáctico o árboles sintácticos.
En la siguiente figura se muestra el árbol de análisis sintáctico de la siguiente proposición:
h:= x + y – x * y
Asignación
identificador
expresión
identificador
expresión
identificador
expresión
identificador
h
:=
x
+
Docente: Ing. Mirko Manrique Ronceros
y
–
x
expresión
*
y
~ 12 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Después del análisis semantico y el de tipos. El análisis semantico es mucho más difícil que el
sintáctico, pues hay que considerar el significado de una unidad gramatical; es decir hay que
interpretarla. Esto se puede lograr traduciendo la entrada a una forma de representación
intermedia. Por ejemplo, nunca hubiéramos definido la variable h de la figura, la proposición de
asignación no tendría sentido. En forma análoga, la asignación de una variable booleana a una
variable real tampoco tendrá sentido. Este tipo de inconsistencias será reconocido por el análisis
de tipos.
El código objeto se genera en la última fase de la compilación: el generador de código. En esta
fase el código intermedio se transforma en código de maquina y la memoria necesaria quedara
determinada. Obviamente, esta es la única fase que depende del hardware, ya que por lo general,
los conjuntos de instrucciones varían de un computador a otro.
Ambigüedad
Se ha de tener cuidado al considerar la estructura de una cadena según una gramática. Aunque
es evidente que cada árbol de análisis sintáctico deriva exactamente la cadena que leer en sus
hojas, una gramática puede tener más de un árbol de análisis sintáctico que genere una cadena
dada de componentes léxicos. Esta clase de gramática se dice que es ambigua. Para demostrar
que una gramática es ambigua, lo único que se requiere es encontrar una cadena de componentes
léxicos que tenga más de un árbol de análisis sintáctico. Como una cadena que cuenta con más de
un árbol de análisis sintáctico suele tener más de un significado, para aplicaciones de compilación
es necesario diseñar gramáticas no ambiguas o utilizar gramáticas ambiguas con reglas adicionales
para resolver las ambigüedades.
Por ejemplo, si se tiene la expresión 9 – 5 + 2 tiene ahora más de un árbol de análisis sintáctico.
Los dos árboles de 9 – 5 + 2 corresponden a dos formas de agrupamientos entre paréntesis de la
expresión : (9 – 5) + 2 y 9 – (5 + 2). Esta segunda forma de agrupamiento entre paréntesis da a
la expresión el valor de 2, en lugar del valor acostumbrado 6.
Asociatividad de operadores
Por convención, 9 + 5 + 2 es equivalente a (9 + 5) + 2, y 9 – 5 – 2 es equivalente a (9 – 5) – 2.
Cuando un operando con 5 tiene operadores a su izquierda y derecha, se necesitan convenciones
para decidir que operador considera es operando. Se dice que el operador + asocia a la izquierda,
porque un operando que tenga un signo + a ambos lados es tomado por el operador que esté a
su izquierda. En la mayoría de los lenguajes de programación, los cuatro operadores matemáticos,
adición, sustracción, multiplicación y división son asociativos a la izquierda.
Docente: Ing. Mirko Manrique Ronceros
~ 13 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Algunos operadores comunes, como la exponenciacion son asociativos por la derecha. Otro
ejemplo análogo, el operador de asignación = en C es asociativo por la derecha; en C la expresión
a = b = c; con un operador asociativo por la derecha, son generadas por la siguiente gramática:
derecha
letra = derecha | letra
letra
a|b|...|z
El contraste entre un árbol de análisis sintáctico para un operador asociativo por la izquierda como
– y un árbol de análisis sintáctico para un operador asociativo por la derecha como =, se muestra
la siguiente figura:
expresión
expresión
expresión –
digito
–
digito
derecha
digito
2
5
letra
a
=
derecha
letra
b
9
=
derecha
letra
c
Procedencia de operadores
Considere la expresión 9 + 5 * 2. Hay dos interpretaciones posibles de esta expresión: (9 + 5) * 2
o 9 + (5 * 2). La asociatividad de + y * no resuelve esta ambigüedad. Por esta razón, se necesita
conocer la precedencia relativa de los operadores cuando esté presente más de una clase de
operadores.
Se dice que * tiene mayor precedencia que + si * considera sus operandos antes que lo haga +.
En aritmética elemental, la multiplicación y división tiene mayor precedencia que la adición y
sustracción. Por tanto 5, es considerado por * en 9 + 5 * 2 y en 9 * 5 + 2; es decir las
expresiones son equivalentes a 9 + (5 * 2) y (9 * 5) + 2, respectivamente.
TRADUCCION DIRIGIDA POR LA SINTAXIS
Para traducir una construcción de un lenguaje de programación, un compilador puede necesitar
tener en cuenta muchas características, además del código generado para la construcción. Por
ejemplo, puede ocurrir que el compilador necesite conocer el tipo de la construcción, la posición
de la primera instrucción del código objeto o el numero de instrucciones generadas. Por tanto, los
atributos asociados con las construcciones se mencionan de manera abstracta. Un atributo puede
representar cualquier cantidad, por ejemplo, una expresión, una cadena, una posición de memoria
o cualquier otra cosa.
Docente: Ing. Mirko Manrique Ronceros
~ 14 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Notación Posfija
La notación posfija de una expresión E se puede definir de manera inductiva como sigue:

Si E es una variable o una constante, entonces la notación posfija de E es también E.

Si E es una expresión de la forma E1 op E2, donde op es cualquier operador binario,
entonces la notación posfija de E es E’1E’2op, donde E’1 y E’2 son las notación posfijas de
E1 y E2 respectivamente.

Si E es una expresión de la forma (E1), entonces la notación posfija de E1 es también la
notación posfija de E.
La notación posfija no necesita paréntesis, porque la posición y la ariedad (numero de
argumentos) de los operadores permiten solo una descodificación de una expresión posfija. Por
ejemplo, la notación posfija de (9 – 5) + 2 es 95-2+ y la notación posfija de 9-(5+2) es 952+-.
Docente: Ing. Mirko Manrique Ronceros
~ 15 ~
Universidad Nacional del Santa
Docente: Ing. Mirko Manrique Ronceros
Curso: Teoría de Compiladores
~ 16 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
EXPRESIONES REGULARES Y AUTOMATAS
La fase de rastreo, o análisis léxico, de un compilador tiene la tarea de leer el
programa fuente como un archivo de caracteres y dividirlo en tokens. Los tokens son
como las palabras de un lenguaje natural: cada token es una secuencia de caracteres
que representa una unidad de información en el programa fuente. Ejemplos típicos de
token son las palabras reservadas, como if y while, las cuales son cadenas fijas de
letras; los identificadores, que son cadenas definidas por el usuario, compuestas por
lo regular de letras y números, y que comienzan con una letra; los símbolos
especiales, como los símbolos aritméticos + y *; además de algunos símbolos
compuestos de múltiples caracteres, tales como > = y <>. En cada caso un token
representa cierto patrón de caracteres que el analizador léxico reconoce, o ajusta
desde el inicio de los caracteres de entrada restantes.
Como la tarea que realiza el analizador léxico es un caso especial de coincidencia
de patrones, necesitamos estudiar métodos de especificación y reconocimiento de
patrones en la medida en que se aplican al proceso de análisis léxico. Estos métodos
son principalmente los de las expresiones regulares y los autómatas finitos. Sin
embargo, un analizador léxico también es la parte del compilador que maneja la
entrada del código fuente, y puesto que esta entrada a menudo involucra un
importante gasto de tiempo, el analizador léxico debe funcionar de manera tan
eficiente como sea posible. Por lo tanto, también necesitamos poner mucha atención a
los detalles prácticos de la estructura del analizador léxico.
Dividiremos el estudio de las cuestiones del analizador léxico como sigue. En primer
lugar, daremos una perspectiva general de la función de un analizador léxico y de las
estructuras y conceptos involucrados. Enseguida, estudiaremos las expresiones
regulares
y por último el estudio de las máquinas de estados finitos o autómatas
finitos.
EL PROCESO DEL ANÁLISIS LÉXICO
El trabajo del analizador léxico es leer los caracteres del código fuente y formarlos en
unidades lógicas para que lo aborden las partes siguientes del compilador
(generalmente el analizador sintáctico). Las unidades lógicas que genera el
analizador léxico se denominan tokens, y formar caracteres en tokens es muy
parecido a formar palabras a partir de caracteres en una oración en un lenguaje
Docente: Ing. Mirko Manrique Ronceros
~ 17 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
natural como el inglés o cualquier otro y decidir lo que cada palabra significa. En esto
se asemeja a la tarea del deletreo.
Los tokens son entidades lógicas que por lo regular se definen como un tipo
enumerado. Por ejemplo, pueden definirse en C como:
typedef enum
{IF,THEN,ELSE,PLUS,MINUS,NUM,ID,...}
TokenType;
Los tokens caen en diversas categorías, una de ellas la constituyen las palabras
reservadas, como IF y THEN, que representan las cadenas de caracteres "if' y
"then". Una segunda categoría es la de los símbolos especiales, como los símbolos
aritméticos MÁS y MENOS, los que se representan con los caracteres "+" y "—".
Finalmente, existen tokens que pueden representar cadenas de múltiples caracteres.
Ejemplos de esto son NUM e ID, los cuales representan números e identificadores.
Los tokens como entidades lógicas se deben distinguir claramente de las cadenas
de caracteres que representan. Por ejemplo, el token de la palabra reservada IF se
debe distinguir de la cadena de caracteres "if' que representa. Para hacer clara la
distinción, la cadena de caracteres representada por un token se denomina en
ocasiones su valor de cadena o su lexema. Algunos tokens tienen sólo un lexema:
las palabras reservadas tienen esta propiedad. No obstante, un token puede
representar un número infinito de lexemas. Los identificadores, por ejemplo, están
todos representados por el token simple ID, pero tienen muchos valores de cadena
diferentes que representan sus nombres individuales. Estos nombres no se pueden
pasar por alto, porque un compilador debe estar al tanto de ellos en una tabla de
símbolos. Por consiguiente, un rastreador o analizador léxico también debe construir
los valores de cadena de por lo menos algunos de los tokens.
EXPRESIONES REGULARES
Las expresiones regulares representan patrones de cadenas de caracteres. Una expresión
regular r se encuentra completamente definida mediante el conjunto de cadenas con las
que concuerda. Este conjunto se denomina lenguaje generado por la expresión
regular y se escribe como L(r), Aquí la palabra lenguaje se utiliza sólo para definir
"conjunto de cadenas" y no tiene (por lo menos en esta etapa) una relación específica con
un lenguaje de programación. Este lenguaje depende, en primer lugar, del conjunto de
caracteres que se encuentra disponible. En general, estaremos hablando del conjunto de
caracteres ASCII o de algún subconjunto del mismo. En ocasiones el conjunto será más
general que el conjunto de caracteres ASCII, en cuyo caso los elementos del conjunto se
Docente: Ing. Mirko Manrique Ronceros
~ 18 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
describirán como símbolos. Este conjunto de símbolos legales se conoce como alfabeto
y por lo general se representa mediante el símbolo griego Σ (sigma).
Una expresión regular r también contendrá caracteres del alfabeto, pero esos
caracteres tendrán un significado diferente: en una expresión regular todos los símbolos
indican patrones. En este capítulo distinguiremos el uso de un carácter como patrón
escribiendo todo los patrones en negritas. De este modo, a es el carácter a usado como
patrón.
Por último, una expresión regular r puede contener caracteres que tengan
significados especiales. Este tipo de caracteres se llaman metacaracteres o
metasímbolos, y por lo general no pueden ser caracteres legales en el alfabeto,
porque no podríamos distinguir su uso como metacaracteres de su uso como
miembros del alfabeto. Sin embargo, a menudo no es posible requerir tal exclusión,
por lo que se debe utilizar una convención para diferenciar los dos usos posibles de un
metacaracter. En muchas situaciones esto se realiza mediante el uso de un carácter
de escape que "desactiva" el significado especial de un metacaracter. Unos
caracteres de escape comunes son la diagonal inversa y las comillas. Advierta que los
caracteres de escape, si también son caracteres legales en el alfabeto, son por sí
mismos metacaracteres.
Docente: Ing. Mirko Manrique Ronceros
~ 19 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Definición de expresiones regulares
Expresiones regulares básicas: Estas son precisamente los caracteres simples del
alfabeto, los cuales se corresponden a sí mismos. Dado cualquier carácter a del
alfabeto Σ, indicamos que la expresión regular a corresponde al carácter a escribiendo
L(a) = {a}. Existen otros dos símbolos que necesitaremos en situaciones especiales.
Necesitamos poder indicar una concordancia con la cadena vacía, es decir, la cadena
que no contiene ningún carácter. Utilizaremos el símbolo ε (épsilon) para denotar la
cadena vacía, y definiremos el metasímbolo ε (e en negritas) estableciendo que L(ε) =
{ ε } . También necesitaremos ocasionalmente ser capaces de describir un símbolo que
corresponda a la ausencia de cadenas, es decir, cuyo lenguaje sea el conjunto vacío,
el cual escribiremos como { }. Emplearemos para esto el símbolo φ y escribiremos
L(φ) = { }. Observe la diferencia entre { } y {ε}: el conjunto { } no contiene ninguna
cadena, mientras que el conjunto {ε} contiene la cadena simple que no se compone
de ningún carácter.
Operaciones de expresiones regulares: Existen tres operaciones básicas en las
expresiones regulares: 1) selección entre alternativas, la cual se indica mediante el
metacaracter
|
(barra
vertical);
2)
concatenación,
que
se
indica
medíante
yuxtaposición (sin un metacaracter), y 3) repetición o "cerradura", la cual se indica
mediante el metacaracter *. Analizaremos cada una por turno, proporcionando la
construcción
del
conjunto
correspondiente
para
los
lenguajes
de
cadenas
concordantes.
Selección entre alternativas: Si r y s son expresiones regulares, entonces r|s es una
expresión regular que define cualquier cadena que concuerda con r o con s. En
términos de lenguajes, el lenguaje de r | s es la unión de los lenguajes de r y s, o L(r
| s) = L(r) u L(s). Como un ejemplo simple, considere la expresión regular a | b: ésta
corresponde tanto al carácter a como al carácter b, es decir, L(a | b) = L(a) U L(b) =
{a} u {b} = {a, b}. Como segundo ejemplo, la expresión regular a | ε corresponde
tanto al carácter simple a como a la cadena vacía (que no está compuesta por ningún
carácter). En otras palabras, L(a | ε) = {a, ε}.
La selección se puede extender a más de una alternativa, de manera que, por
ejemplo, L(a | b | c | d) = {a, b, c, d}. En ocasiones también escribiremos largas secuencias
de selecciones con puntos, como en a | b | ... | z, que corresponde a cualquiera de las
letras minúsculas de la a a la z.
Concatenación: La concatenación de dos expresiones regulares r y s se escribe como rs, y
corresponde a cualquier cadena que sea la concatenación de dos cadenas, con la primera
Docente: Ing. Mirko Manrique Ronceros
~ 20 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
de ellas correspondiendo a r y la segunda correspondiendo a s. Por ejemplo, la expresión
regular ab corresponde sólo a la cadena ab, mientras que la expresión regular (a | b) c
corresponde a las cadenas ac y bc. (El uso de los paréntesis como metacaracteres en
esta expresión regular se explicará en breve).
Podemos describir el efecto de la concatenación en términos de lenguajes generados
al definir la concatenación de dos conjuntos de cadenas. Dados dos conjuntos de
cadenas S1 y S2, el conjunto concatenado de cadenas S1S2 es el conjunto de cadenas
de S1 complementado con todas las cadenas de S2. Por ejemplo, si S1 = {aa, b} y S2 =
{a, bb}, entonces S1S2 = {aaa, aabb, ba, bbb}. Ahora la operación de concatenación para
expresiones regulares se puede definir como sigue: L(rs)=L(r)L(s). De esta manera
(utilizando nuestro ejemplo anterior), L{(a | b) c) = L(a | b)L(c) = {a, b } { c ) = {ac,
bc}.
La concatenación también se puede extender a más de dos expresiones regulares: L(r¡
r2 . . . r„) = L(ri)L(r2) . . . L(rn) = el conjunto de cadenas formado al concatenar todas las
cadenas de cada una de las L(r1), . . . , L(rn).
Repetición: La operación de repetición de una expresión regular, denominada también en
ocasiones cerradura (de Kleene), se escribe r*, donde r es una expresión regular. La
expresión regular r* corresponde a cualquier concatenación finita de cadenas, cada una
de las cuales corresponde a r. Por ejemplo, a* corresponde a las cadenas e, a, aa, aaa,
.... (Concuerda con e porque e es la concatenación de ninguna cadena concordante con
a.) Podemos definir la operación de repetición en términos de lenguajes generados
definiendo, a su vez, una operación similar * para conjuntos de cadenas. Dado un
conjunto S de cadenas, sea:
S* = {e} uSuSSuSSSu...
Ahora podemos definir la operación de repetición para expresiones regulares como
sigue:
L{r*) = L(r)*
Considere como ejemplo la expresión regular (a | bb) *. (De nueva cuenta, la razón
de tener paréntesis como metacaracteres se explicará más adelante.) Esta expresión
regular corresponde a cualquiera de las cadenas siguientes: e, a, bb, aa, abb, bba,
bbbb, aaa, aabb y así sucesivamente. En términos de lenguajes, L( (a | bb) *) = L(a |
bb)* = [a, bb}* = {ε, a, bb, aa, abb, bba, bbbb, aaa, aabb, abba, abbbb, bbaa, . . .}.
Docente: Ing. Mirko Manrique Ronceros
~ 21 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Precedencia de operaciones y el uso de los paréntesis La descripción precedente
no toma en cuenta la cuestión de la precedencia de las operaciones de elección,
concatenación y repetición. Por ejemplo, dada la expresión regular a | b*,
¿deberíamos interpretar esto como (a | b) * o como a|(b*) ? (Existe una diferencia
importante, puesto que L( (a |b) *) = {ε, a, b, aa, ab, ba, bb, .. .}, mientras que L(a
| (b*)) = {ε, a, b, bb, bbb, . . .}.) La convención estándar es que la repetición debería
tener mayor precedencia, por lo tanto, la segunda interpretación es la correcta. En
realidad, entre las tres operaciones, se le da al * la precedencia más alta, a la
concatenación se le da la precedencia que sigue y a la | se le otorga la precedencia
más baja. De este modo, por ejemplo, a | bc* se interpreta como a|(b (c * ) ),
mientras que ab | c*d se interpreta como (ab) | ( ( c * ) d ) .
Cuando deseemos indicar una precedencia diferente, debemos usar paréntesis para
hacerlo. Ésta es la razón por la que tuvimos que escribir (a|b)c para indicar que la
operación de elección debería tener mayor precedencia que la concatenación, ya que
de otro modo a | bc se interpretaría como si correspondiera tanto a a como a bc. De
manera similar, (a l bb) * se interpretaría sin los paréntesis como a | bb*, lo que
corresponde a a, b, bb, bbb, .... Los paréntesis aquí se usan igual que en aritmética,
donde (3 + 4) * 5 = 35, pero 3 + 4 * 5 = 23, ya que se supone que * tiene
precedencia más alta que +.
Nombres para expresiones regulares A menudo es útil como una forma de simplificar
la notación proporcionar un nombre para una expresión regular larga, de modo que no
tengamos que escribir la expresión misma cada vez que deseemos utilizarla. Por
ejemplo, si deseáramos desarrollar una expresión regular para una secuencia de uno
o más dígitos numéricos, entonces escribiríamos
(0|1|2| ... |9)(0|1|2| ... |9)* o podríamos escribir
dígito dígito* donde
dígito =
0I1I2I...I9
es una definición regular del nombre dígito.
El uso de una definición regular es muy conveniente, pero introduce la complicación
agregada de que el nombre mismo se convierta en un metasímbolo y se deba
encontrar un significado para distinguirlo de la concatenación de sus caracteres. En
nuestro caso hicimos esa distinción al utilizar letra cursiva para el nombre. Advierta
que no se debe emplear el nombre del término en su propia definición (es decir, de
manera recursiva): debemos poder eliminar nombres reemplazándolos sucesivamente
con las expresiones regulares para las que se establecieron.
Docente: Ing. Mirko Manrique Ronceros
~ 22 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Antes de considerar una serie de ejemplos para elaborar nuestra definición de
expresiones regulares, reuniremos todas las piezas de la definición de una expresión
regular.
Una expresión regular es una de las siguientes:
1. Una expresión regular básica constituida por un solo carácter a, donde a proviene de
un alfabeto Σ de caracteres legales; el metacarácter ε ; o el metacarácter ε. En el primer caso, L(a) = {a}; en el segundo, L(ε) = {ε}; en el tercero, L(φ) = {}.
2. Una expresión de la forma r | s, donde r y s son expresiones regulares. En este caso,
L(r | s) = L(r) u L(s).
3. Una expresión de la forma rs, donde r y s son expresiones regulares. En este caso,
L(rs) = L(r)L(s).
4. Una expresión de la forma r*, donde r es una expresión regular. En este caso,
L(r*)=L(r)*.
5. Una expresión de la forma (r), donde r es una expresión regular. En este caso,
L((r)) = L(r). De este modo, los paréntesis no cambian el lenguaje, sólo se utilizan
para ajustar la precedencia de las operaciones.
Ejemplo1:
Consideremos el alfabeto simple constituido por sólo tres caracteres alfabéticos: Σ= {a,
b,c). También el conjunto de todas las cadenas en este alfabeto que contengan
exactamente una b. Este conjunto es generado por la expresión regular
(alc)*b(alc)*
Advierta que, aunque b aparece en el centro de la expresión regular, la letra b no
necesita estar en el centro de la cadena que se desea definir. En realidad, la repetición de
a o c antes y después de la b puede presentarse en diferentes números de veces. Por
consiguiente, todas las cadenas siguientes están generadas mediante la expresión regular
anterior: b, abc, abaca, baaaac, ccbaca, ccccccb.
Ejemplo2:
Con el mismo alfabeto que antes, considere el conjunto de todas las cadenas que
contienen como máximo una b. Una expresión regular para este conjunto se puede
obtener utilizando la solución al ejemplo anterior como una alternativa (definiendo
aquellas cadenas con exactamente una b) y la expresión regular ( a l c ) * como la otra
alternativa (definiendo los casos sin b en todo). De este modo, tenemos la solución
siguiente:
(alc)*|( a l c ) * b ( a | c ) *
Docente: Ing. Mirko Manrique Ronceros
~ 23 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Una solución alternativa sería permitir que b o la cadena vacía apareciera entre las
dos repeticiones de a o c:
( a |c )* (b l ε ) (alc)*
Ejemplo3:
Consideremos el conjunto de cadenas S sobre el alfabeto Σ = {a,b} compuesto de
una b simple rodeada por el mismo número de a:
n n
S = {b, aba, aabaa, aaabaaa, . . .} = {a ba |n ≠ 0}
Docente: Ing. Mirko Manrique Ronceros
~ 24 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
AUTÓMATAS FINITOS
Los autómatas finitos, o máquinas de estados finitos, son una manera matemática para
describir clases particulares de algoritmos (o "máquinas"). En particular, los autómatas
finitos se pueden utilizar para describir el proceso de reconocimiento de patrones en
cadenas de entrada, y de este modo se pueden utilizar para construir analizadores
léxicos. Por supuesto, también existe una fuerte relación entre los autómatas finitos y las
expresiones regulares, y veremos en la sección siguiente cómo construir un autómata
finito a partir de una expresión regular. Sin embargo, antes de comenzar nuestro estudio
de los autómatas finitos de manera apropiada, consideraremos un ejemplo explicativo.
El patrón para identificadores como se define comúnmente en los lenguajes de programación está dado por la siguiente definición regular (supondremos que letra y dígito
ya se definieron):
identificador =
letra(letra|dígito)*
Esto representa una cadena que comienza con una letra y continúa con cualquier
secuencia de letras y/o dígitos. El proceso de reconocer una cadena así se puede describir
mediar diagrama de la figura:
En este diagrama los círculos numerados 1 y 2 representan estados, que son localidades
en proceso de reconocimiento que registran cuánto del patrón ya se ha visto. Las líneas
flechas representan transiciones que registran un cambio de un estado a otro en una
coincidencia del carácter o caracteres mediante los cuales son etiquetados. En el
diagrama muestra, el estado 1 es el estado de inicio, o el estado en el que comienza el
proceso de reconocimiento. Por convención, el estado de inicio se indica dibujando una
línea con flecha sin etiqueta que proviene de "de ninguna parte". El estado 2 representa
el punto en el cual se ha igualado una sola letra (lo que se indica mediante la transición
del estado 1 al estado 2 etiquetada con letra). Una vez en el estado 2, cualquier
número de letras y/o dígitos se puede ver, y una coincidencia de éstos nos regresa al
Docente: Ing. Mirko Manrique Ronceros
~ 25 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
estado 2. Los estados que representan el fin del proceso de reconocimiento, en los cuales
podemos declarar un éxito, se denominan estados de aceptación, y se indican
dibujando un borde con línea doble alrededor del estado en el diagrama. Puede haber
más de uno de éstos. En el diagrama de muestra el estado 2 es un estado de aceptación,
lo cual indica que, después que cede una letra, cualquier secuencia de letras y dígitos
subsiguiente (incluyendo la ausencia de todas) representa identificador legal.
El proceso de reconocimiento de una cadena de caracteres real como un identificador
ahora se puede indicar al enumerar la secuencia de estados y transiciones en el diagrama
q se utiliza en el proceso de reconocimiento. Por ejemplo, el proceso de reconocer
xtemp como un identificador se puede indicar como sigue:
Un DFA (por las siglas del concepto autómata finito determinístico en inglés) M se
compone de un alfabeto Σ, un conjunto de estados S, una función de transición T: S X
Σ —> S, un estado de inicio s0 ε S y un conjunto de estados de aceptación A C S. El
lenguaje aceptado por M, escrito como L(M), se define como el conjunto de cadenas
de caracteres C1C2.. .c„ con cada c¡  Σ, tal que existen estados Sj = T(s0, C1), s2 =
T(s1, c2),... ,sn = T(Sn-1, cn), con sn como un elemento de A (es decir, un estado de
aceptación).
Hacemos las anotaciones siguientes respecto a esta definición. S X Σ se refiere al
producto cartesiano o producto cruz de S y Σ: el conjunto de pares (s, c), donde
s  S y c  Σ La función T registra las transiciones: T(s, c) = s' si existe una transición
del estado S al estado s' etiquetado mediante c. El segmento correspondiente del
diagrama para M tendrá el aspecto siguiente:
La aceptación como la existencia de una secuencia de estados s1 = T(so, c1), s2 =
T(s1, c2),. . . , = T(sn-1, c„), con sn siendo un estado de aceptación, significa
entonces lo mismo que el
Docente: Ing. Mirko Manrique Ronceros
~ 26 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Advertimos
un número de diferencias entre la definición de un DFA y el diagrama del ejemplo
identificador. En primer lugar, utilizamos los números para los estados en el diagrama
del identificador, mientras la definición no restrinja el conjunto de estados a números.
En realidad, podemos emplear cualquier sistema de identificación que queramos para
los estados, incluyendo nombres. Por ejemplo, podemos escribir un diagrama
equivalente al de la figura como:
donde ahora denominamos a los estados inicio (porque es el estado de inicio) y
entrada_id (porque vimos una letra y estará reconociendo un identificador después de
letras y números subsiguientes cualesquiera). El conjunto de estados para este
diagrama se convierte ahora en {inicio, entrada_jd} en lugar de {1, 2}.
Una segunda diferencia entre el diagrama y la definición es que no etiquetamos las
transiciones con caracteres sino con nombres que representan un conjunto de
caracteres.
El conjunto de cadenas que contienen exactamente una b es aceptado por el siguiente
DFA:
El conjunto de cadenas que contienen como máximo una b es aceptado por el
siguiente DFA:
Docente: Ing. Mirko Manrique Ronceros
~ 27 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Definiciones regulares para constantes numéricas en notación científica como se
muestra a continuación
Un NFA (por las siglas del término autómata finito no determinístico en inglés) M
consta de un alfabeto Σ, un conjunto de estados S y una función de transición T:S X(Σ
u {ε}) -> φ(S), así como de un estado de inicio s0 de S y un conjunto de estados de
aceptación A de S. El lenguaje aceptado por M, escrito como L(M), se define como el
conjunto de cadenas de caracteres
estados
s1
en T(s0,
c1), s2
c1c2.
en T(s1,
. .cn con cada
c2),.
..,
sn
c¡
de Σ u {ε} tal que existen
en T(sn-1
cn),
con Sn como un
elemento de A.
Considere el siguiente diagrama de un NFA
La cadena abb puede ser aceptada por cualquiera de las siguientes secuencias de
transacciones:
Docente: Ing. Mirko Manrique Ronceros
~ 28 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
En realidad las transiciones del estado 1 al estado 2 en a, y del estado 2 al estado 4 en b,
permiten que la máquina acepte la cadena ab, y entonces, utilizando la transición ε del
estado 4 al estado 2, todas las cadenas igualan la expresión regular ab+. De manera
similar, las transiciones del estado 1 al estado 3 en a, y del estado 3 al estado 4 en ε,
permiten la aceptación de todas las cadenas que coinciden con ab*. Finalmente,
siguiendo la transición e desde el estado 1 hasta el estado 4 se permite la aceptación de
todas las cadenas coincidentes con b*. De este modo, este NFA acepta el mismo
lenguaje que la expresión regular
ab+ l ab* I b*. Una expresión regular más simple
que genera el mismo lenguaje es (a|ε)b*. El siguiente DFA también acepta este
lenguaje:
Considere el siguiente NFA
Docente: Ing. Mirko Manrique Ronceros
~ 29 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Este acepta la cadena acab al efectuar las transiciones siguientes:
Docente: Ing. Mirko Manrique Ronceros
~ 30 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
LENGUAJES FORMALES
En matemáticas, lógica, y ciencias de la computación, un lenguaje formal es un
conjunto de palabras (cadenas de caracteres) de longitud finita en los casos más
simples o expresiones válidas (formuladas por palabras) formadas a partir de un
alfabeto (conjunto de caracteres) finito. El nombre lenguaje se justifica porque las
estructuras que con este se forman tienen reglas de buena formación (gramática)
e interpretación semántica (significado) en una forma muy similar a los lenguajes
hablados.
Un posible alfabeto sería, digamos,
alfabeto sería, por ejemplo,
, y una cadena cualquiera sobre este
. Un lenguaje sobre este alfabeto, que incluyera
esta cadena, sería: el conjunto de todas las cadenas que contienen el mismo número
de símbolos
que , por ejemplo.
La palabra vacía (esto es, la cadena de longitud cero) se permite en este tipo de
lenguajes, notándose frecuentemente mediante
,
ó
. A diferencia de que ocurre
con el alfabeto (que es un conjunto finito) y con cada palabra (que tiene una longitud
también finita), un lenguaje puede estar compuesto por un número infinito de
palabras.
Ejemplos de lenguajes formales:

El conjunto de todas las palabras sobre
.

El conjunto

El conjunto de todos los programas sintácticamente válidos en un determinado
es un número primo.
lenguaje de programación.

El conjunto de sentencias bien formadas en lógica de predicados.
Especificación de lenguajes formales
Los lenguajes formales se pueden especificar de una amplia variedad de formas, como
por ejemplo:

Cadenas producidas por una gramática formal (véase Jerarquía de Chomsky).

Cadenas producidas por una expresión regular.

Cadenas aceptadas por un autómata, tal como una máquina de Turing.
Docente: Ing. Mirko Manrique Ronceros
~ 31 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Operaciones
Se pueden utilizar varias operaciones para producir nuevos lenguajes a partir de otros
dados. Supóngase que L1 y L2 son lenguajes sobre un alfabeto común. Entonces:

La concatenación L1L2 consiste de todas aquellas palabras de la forma vw donde
v es una palabra de L1 y w es una palabra de L2

La intersección L1&L2 consiste en todas aquellas palabras que están contenidas
tanto en L1 como en L2

La unión L1|L2 consiste en todas aquellas palabras que están contenidas ya sea
en L1 o en L2

El complemento ~L1 consiste en todas aquellas palabras producibles sobre el
alfabeto de L1 que no están ya contenidas en L1

El cociente L1/L2 consiste de todas aquellas palabras v para las cuales existe
una palabra w en L2 tales que vw se encuentra en L1

La estrella L1* consiste de todas aquellas palabras que pueden ser escritas de la
forma W1W2...Wn donde todo Wi se encuentra en L1 y n ≥ 0. (Nótese que esta
definición incluye a ε en cualquier L*)

La intercalación L1*L2 consiste de todas aquellas palabras que pueden ser
escritas de la forma v1w1v2w2...vnwn; son palabras tales que la concatenación
v1...vn está en L1, y la concatenación w1...wn está en L2
Por contraposición al lenguaje propio de los seres vivos y en especial el lenguaje
humano, considerados lenguajes naturales, se denomina lenguaje formal a los
lenguajes «artificiales» propios de las matemáticas o la informática, los lenguajes
artificiales son llamados lenguajes formales (incluyendo lenguajes de programación).
Sin embargo, el lenguaje humano tiene una característica que no se encuentra en los
lenguajes de programación: la diversidad.
En 1956, Noam Chomsky creó la Jerarquía de Chomsky para organizar los distintos
tipos de lenguaje formal.
Docente: Ing. Mirko Manrique Ronceros
~ 32 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Verdades concernientes a los lenguajes formales
Teorema 1: El conjunto de lenguajes en general (incluyendo los no-formales) es
incontable.
Lema 1: El conjunto de lenguajes en un alfabeto no vacío dado es incontable
Afirmar que un alfabeto es no-vacío equivale a que ese alfabeto contenga al menos un
símbolo, Basta demostrar que el conjunto de lenguajes en el alfabeto
incontable. Como sabemos, un lenguaje L en
es un subconjunto de
, esto nos
lleva a la conclusión de que, el conjunto de todos los lenguajes en
es justamente
(el conjunto de todos los subconjuntos o conjunto potencia de
evidente que
A
) y es
es infinito (de hecho; contable), también ha sido demostrado que si
A es un conjunto infinito (contable o incontable), entonces 2
2
es
A
es mayor que A porque
pasa a ser un conjunto infinito de ordenes del infinito, al ser mayor, no existirá
A
A
biyección entre A y 2 , lo que hace a 2 un conjunto infinito incontable.
Demostración del Teorema 1: Puede derivarse fácilmente que la aseveración
delineada en el Teorema 1 es verdadera, porque el conjunto de lenguajes en general
A
es justamente una unión infinita de conjuntos del tipo 2 , donde A es un conjunto
infinito contable.
Teorema 2: Los lenguajes son conjuntos contables
Se sabe que un lenguaje L en un alfabeto Σ es un subconjunto de Σ
hizo mención, Σ
*
*
y como ya se
es infinito incontable, por ende, L es como mucho un conjunto
infinito incontable (del mismo tamaño que Σ *.
Teorema 3: El conjunto de lenguajes formales es contable
Como sabemos un lenguaje formal puede ser generado por una gramática formal (o
de estructura de frase), lo cual implica que todo lenguaje formal puede ser aceptado
por una Máquina de Turing(MT), lo que a su vez implica que se puede definir una
biyección entre el conjunto de lenguajes formales y el conjunto de las MT´s (debido a
la propiedad transitiva de la relación "existe biyección entre A y B"). Para demostrar el
teorema se utilizará el concepto de codificación de MT´s que se introduce en el
estudio de las MT´s universales, generalmente se codifica una MT con una función que
Docente: Ing. Mirko Manrique Ronceros
~ 33 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
tiene precisamente como dominio al conjunto de las MT´s (lo llamaremos X) y como
codominio
, esa función puede ser una biyección si el codominio pasa a ser Y
(un subconjunto de
) y como
es contable, ese subconjunto también
será contable y como existe dicha biyección (entre X e Y).
Gramática formal
Una gramática formal es un objeto o modelo matemático que permite especificar un
lenguaje o lengua, es decir, es el conjunto de reglas capaces de generar todas las
posibilidades combinatorias de ese lenguaje, ya sea éste un lenguaje formal o un
lenguaje natural.
Introducción
El elemento en mayúsculas es el símbolo inicial. Los elementos en minúsculas son
símbolos terminales. Las cadenas de la lengua son aquellas que solo contienen
elementos terminales, como por ejemplo:
bbbdeccc, de, bdec, ... Estas serían tres posibles realizaciones del lenguaje cuya
gramática hemos definido con dos reglas.
Para comprender mejor el concepto pondremos algunas reglas de la gramática
castellana:

Una
FRASE
se
puede
componer
de
SUJETO
+
PREDICADO
O = SN + SV

Un SUJETO se puede componer de un ARTÍCULO + NOMBRE o SUSTANTIVO
(núcleo) + Complementos
SN = Det + N + C

Un
PREDICADO
se
puede
componer
de
un
VERBO
conjugado
SV = Aux + GV

Un ARTICULO puede ser la palabra "el"

Un NOMBRE o SUBSTANTIVO puede ser "niño"
Vemos que existen unas definiciones especiales como FRASE, SUJETO, etc. que no
aparecen en la frase final formada. Son unas entidades abstractas denominadas
Categorías Sintácticas que no son utilizables en una frase.
Docente: Ing. Mirko Manrique Ronceros
~ 34 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Las categorías sintácticas definen la estructura del lenguaje representando porciones
más o menos grandes de las frases. Existe una jerarquía interna entre las categorías
sintácticas.
La categoría superior sería la FRASE que representa una oración válida en lengua
castellana.
Por debajo de ella se encuentran sus componentes. Ninguna de estas categorías da
lugar a frases válidas solo la categoría superior.
Al finalizar toda la jerarquía llegamos a las palabras que son las unidades mínimas con
significado que puede adoptar una frase.
Aplicando las jerarquías y sustituyendo elementos, llegamos al punto en donde todas
las categorías sintácticas se han convertido en palabras, obteniendo por tanto una
oración VÁLIDA. (Como por ejemplo: El niño corre). Este proceso se llama producción
o generación.
En resumen:
Elementos constituyentes

Una gramática formal es un modelo matemático compuesto por una serie de
categorías sintácticas que se combinan entre sí por medio de unas reglas
sintácticas que definen cómo se crea una categoría sintáctica por medio de
otras o símbolos de la gramática.

Existe una única categoría superior que denota cadenas completas y válidas.
Mecanismos de especificación

Por medio de estos elementos constituyentes se define un mecanismo de
especificación consistente en repetir el mecanismo de sustitución de una
categoría por sus constituyentes en función de las reglas comenzando por la
categoría superior y finalizando cuando la oración ya no contiene ninguna
categoría.
De esta forma, la gramática puede generar o producir cada una de las cadenas del
lenguaje correspondiente y solo estas cadenas.
Docente: Ing. Mirko Manrique Ronceros
~ 35 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Definición
Una Gramática Formal es una cuádrupla
donde:

N es un alfabeto de símbolos no terminales (variables).

T es un alfabeto de símbolos terminales (constantes).

Debe cumplirse que
. denotaremos con
el alfabeto de
la gramática.

es el símbolo inicial o axioma de la gramática.

es el conjunto de reglas de producción, de la forma
β
{ α → β | α
}
Es decir, la cadena α debe contener al menos una variable, que puede estar rodeada
de un contexto.
Derivaciones
Sea G = (N,T,P,S) una gramática, y sean α, β, δ, φ, ρ, ... palabras de Σ

*
. Entonces
β se deriva de α en un paso de derivación, y lo denotamos con α
dos cadenas
β si existen
, y una producción δ → ρ tales que α = φ1 δ φ2, y β =
φ1 ρ φ2

Notamos con
al cierre reflexivo y transitivo de
. Es decir α
β denota a
una secuencia de derivaciones en un número finito de pasos desde α hasta β.
es una forma sentencial de G, si puede obtenerse la siguiente

secuencia de derivaciones
. En el caso particular de que
se dice
que x es una sentencia

Se
denomina
lenguaje
Docente: Ing. Mirko Manrique Ronceros
formal
generado
por
G
al
conjunto
~ 36 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
EXPRESIÓN REGULAR
Una expresión regular, a menudo llamada también patrón, es una expresión que
describe un conjunto de cadenas sin enumerar sus elementos. Por ejemplo, el grupo
formado por las cadenas Handel, Händel y Haendel se describe mediante el patrón
"H(a|ä|ae)ndel". La mayoría de las formalizaciones proporcionan los siguientes
constructores: una expresión regular es una forma de representar a los lenguajes
regulares (finitos o infinitos) y se construye utilizando caracteres del alfabeto sobre el
cual se define el lenguaje. Específicamente, las expresiones regulares se construyen
utilizando los operadores unión concatenación y clausura de Kleene.
alternación
Una barra vertical separa las alternativas. Por ejemplo, "marrón|castaño" casa
con marrón o castaño.
cuantificación
Un cuantificador tras un carácter especifica la frecuencia con la que éste puede
ocurrir. Los cuantificadores más comunes son +, ? y *:
+
El signo más indica que el carácter al que sigue debe aparecer al menos una
vez. Por ejemplo, "ho+la" describe el conjunto infinito hola, hoola, hooola,
hoooola, etcétera.
?
El signo de interrogación indica que el carácter al que sigue puede aparecer
como mucho una vez. Por ejemplo, "ob?scuro" casa con oscuro y obscuro.
*
El asterisco indica que el carácter al que sigue puede aparecer cero, una, o más
veces. Por ejemplo, "0*42" casa con 42, 042, 0042, 00042, etcétera.
agrupación
Los paréntesis pueden usarse para definir el ámbito y precedencia de los demás
operadores. Por ejemplo, "(p|m)adre" es lo mismo que "padre|madre", y
"(des)?amor" casa con amor y con desamor.
Los constructores pueden combinarse libremente dentro de la misma expresión, por lo
que "H(ae?|ä)ndel" equivale a "H(a|ae|ä)ndel".
Docente: Ing. Mirko Manrique Ronceros
~ 37 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
La sintaxis precisa de las expresiones regulares cambia según las herramientas y
aplicaciones consideradas, y se describe con más detalle a continuación.
Su utilidad más obvia es la de describir un conjunto de cadenas, lo que resulta de
utilidad en editores de texto y aplicaciones para buscar y manipular textos. Muchos
lenguajes de programación admiten el uso de expresiones regulares con este fin. Por
ejemplo, Perl tiene un potente motor de expresiones regulares directamente incluido
en su sintaxis. Las herramientas proporcionadas por las distribuciones de Unix
(incluyendo el editor sed y el filtro grep) fueron las primeras en popularizar el
concepto de expresión regular.
Aplicaciones
Numerosos editores de texto y otras utilidades (especialmente en el sistema operativo
UNIX/linux), como por ejemplo sed y awk, utilizan expresiones regulares para, por
ejemplo, buscar palabras en el texto y reemplazarlas con alguna otra cadena de
caracteres.
Las expresiones regulares en programación
En el área de la programación las expresiones regulares son un método por medio del
cual se pueden realizar búsquedas dentro de cadenas de caracteres. Sin importar si la
búsqueda requerida es de dos caracteres en una cadena de 10 o si es necesario
encontrar todas las apariciones de un patrón definido de caracteres en un archivo de
millones de caracteres, las expresiones regulares proporcionan una solución para el
problema. Adicionalmente, un uso derivado de la búsqueda de patrones es la
validación de un formato específico en una cadena de caracteres dada, como por
ejemplo fechas o identificadores.
Para poder utilizar las expresiones regulares al programar es necesario tener acceso a
un motor de búsqueda con la capacidad de utilizarlas. Es posible clasificar los motores
disponibles en dos tipos: Motores para el programador y Motores para el usuario final.
Motores para el usuario final: son programas que permiten realizar búsquedas
sobre el contenido de un archivo o sobre un texto extraído y colocado en el programa.
Están diseñados para permitir al usuario realizar búsquedas avanzadas usando este
mecanismo, sin embargo es necesario aprender a redactar expresiones regulares
adecuadas para poder utilizarlos eficientemente. Éstos son algunos de los programas
disponibles:
Docente: Ing. Mirko Manrique Ronceros
~ 38 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores

grep: programa de los sistemas operativos Unix/Linux

PowerGrep: versión de grep para los sistemas operativos Windows

RegexBuddy: ayuda a crear las expresiones regulares en forma interactiva y
luego le permite al usuario usarlas y guardarlas.

EditPad Pro: permite realizar búsquedas con expresiones regulares sobre
archivos y las muestra por medio de código de colores para facilitar su lectura y
comprensión.
Motores para el programador: permiten automatizar el proceso de búsqueda de
modo que sea posible utilizarlo muchas veces para un propósito específico. Estas son
algunas de las herramientas de programación disponibles que ofrecen motores de
búsqueda con soporte a expresiones regulares:

Java: existen varias librerías hechas para java que permiten el uso de RegEx, y
Sun planea dar soporte a estas desde el SDK

JavaScript: a partir de la versión 1.2 (ie4+, ns4+) JavaScript tiene soporte
integrado para expresiones regulares, lo que significa que las validaciones que
se realizan normalmente en una página web podrían simplificarse grandemente
si el programador supiera utilizar esta herramienta.

Perl: es el lenguaje que hizo crecer a las expresiones regulares en el ámbito de
la programación hasta llegar a lo que son hoy en día.

PCRE: librería de ExReg para C, C++ y otros lenguajes que puedan utilizar
librerías dll (Visual Basic 6 por ejemplo).

PHP: tiene dos tipos diferentes de expresiones regulares disponibles para el
programador.

Python: lenguaje de "scripting" popular con soporte a Expresiones Regulares.

.Net Framework: provee un conjunto de clases mediante las cuales es posible
utilizar expresiones regulares para hacer búsquedas, reemplazar cadenas y
validar patrones.
Nota: de las herramientas mencionadas con anterioridad se utilizan el EditPad Pro y
el .Net Framework para dar ejemplos, aunque es posible utilizar las expresiones
regulares con cualquier combinación de las herramientas mencionadas. Aunque en
general las Expresiones Regulares utilizan un lenguaje común en todas las
herramientas, las explicaciones prácticas acerca de la utilización de las herramientas y
los ejemplos de código deben ser interpretados de forma diferente. También es
Docente: Ing. Mirko Manrique Ronceros
~ 39 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
necesario hacer notar que existen algunos detalles de sintaxis de las expresiones
regulares que son propietarios del .Net Framework que se utilizan en forma diferente
en las demás herramientas de programación. Cuando estos casos se den se hará
notar en forma explícita para que el lector pueda buscar información respecto a estos
detalles en fuentes adicionales. En el futuro se incluirán adicionalmente ejemplos de
otras herramientas y lenguajes de programación.
Expresiones regulares como motor de búsqueda
Las expresiones regulares permiten encontrar porciones específicas de texto dentro de
una cadena más grande de caracteres. Así, si es necesario encontrar el texto "lote" en
la expresión "el ocelote salto al lote contiguo" cualquier motor de búsqueda sería
capaz de efectuar esta labor. Sin embargo, la mayoría de los motores de búsqueda
encontrarían también el fragmento "lote" de la palabra "ocelote", lo cual podría no ser
el resultado esperado. Algunos motores de búsqueda permiten adicionalmente
especificar que se desea encontrar solamente palabras completas, solucionando este
problema. Las expresiones regulares permiten especificar todas estas opciones
adicionales y muchas otras sin necesidad de configurar opciones adicionales, sino
utilizando el mismo texto de búsqueda como un lenguaje que permite enviarle al
motor de búsqueda exactamente lo que deseamos encontrar en todos los casos, sin
necesidad de activar opciones adicionales al realizar la búsqueda.
Expresiones regulares como lenguaje
Para especificar opciones dentro del texto a buscar se utiliza un lenguaje o convención
mediante el cual se le transmite al motor de búsqueda el resultado que se desea
obtener. Este lenguaje le da un significado especial a una serie de caracteres. Por lo
tanto cuando el motor de búsqueda de expresiones regulares encuentre estos
caracteres no los buscará en el texto en forma literal, sino que buscará lo que los
caracteres significan. A estos caracteres se les llama algunas veces "meta-caracteres".
A continuación se listan los principales meta-caracteres y su función y como los
interpreta el motor de expresiones regulares.
Docente: Ing. Mirko Manrique Ronceros
~ 40 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Descripción de las expresiones regulares
El Punto "."
El punto es interpretado por el motor de búsqueda como cualquier otro carácter
excepto los caracteres que representan un salto de línea, a menos que se le
especifique esto al motor de Expresiones Regulares. Por lo tanto si esta opción se
deshabilita en el motor de búsqueda que se utilice, el punto le dirá al motor que
encuentre cualquier carácter incluyendo los saltos de línea. En la herramienta EditPad
Pro esto se hace por medio de la opción "punto corresponde a nueva línea" en las
opciones de búsqueda. En .Net Framework se utiliza la opción RegexOptions.
Singleline al efectuar la búsqueda o crear la expresión regular.
El punto se utiliza de la siguiente forma: Si se le dice al motor de RegEx que busque
"g.t" en la cadena "el gato de piedra en la gótica puerta de getisboro goot" el motor
de búsqueda encontrará "gat", "gót" y por último "get". Nótese que el motor de
búsqueda no encuentra "goot"; esto es porque el punto representa un solo carácter y
únicamente uno. Si es necesario que el motor encuentre también la expresión "goot",
será necesario utilizar repeticiones, las cuales se explican más adelante.
Aunque el punto es muy útil para encontrar caracteres que no conocemos, es
necesario recordar que corresponde a cualquier carácter y que muchas veces esto no
es lo que se requiere. Es muy diferente buscar cualquier carácter que buscar cualquier
carácter alfanumérico o cualquier dígito o cualquier no-dígito o cualquier noalfanumérico. Se debe tomar esto en cuenta antes de utilizar el punto y obtener
resultados no deseados.
La barra inversa o contrabarra "\"
Se utiliza para "marcar" el siguiente carácter de la expresión de búsqueda de forma
que este adquiera un significado especial o deje de tenerlo. O sea, la barra inversa no
se utiliza nunca por sí sola, sino en combinación con otros caracteres. Al utilizarlo por
ejemplo en combinación con el punto "\." este deja de tener su significado normal y
se comporta como un carácter literal.
De la misma forma, cuando se coloca la barra inversa seguida de cualquiera de los
caracteres especiales que discutiremos a continuación, estos dejan de tener su
significado especial y se convierten en caracteres de búsqueda literal.
Docente: Ing. Mirko Manrique Ronceros
~ 41 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Como ya se mencionó con anterioridad, la barra inversa también puede darle
significado especial a caracteres que no lo tienen. A continuación hay una lista de
algunas de estas combinaciones:

\t — Representa un tabulador.

\r — Representa el "regreso al inicio" o sea el lugar en que la línea vuelve a
iniciar.

\n — Representa la "nueva línea" el carácter por medio del cual una línea da
inicio. Es necesario recordar que en Windows es necesaria una combinación de
\r\n para comenzar una nueva línea, mientras que en Unix solamente se usa
\n.

\a — Representa una "campana" o "beep" que se produce al imprimir este
carácter.

\e — Representa la tecla "Esc" o "Escape"

\f — Representa un salto de página

\v — Representa un tabulador vertical

\x — Se utiliza para representar caracteres ASCII o ANSI si conoce su código.
De esta forma, si se busca el símbolo de derechos de autor y la fuente en la
que se busca utiliza el conjunto de caracteres Latin-1 es posible encontrarlo
utilizando "\xA9".

\u — Se utiliza para representar caracteres Unicode si se conoce su código.
"\u00A2" representa el símbolo de centavos. No todos los motores de
Expresiones Regulares soportan Unicode. El .Net Framework lo hace, pero el
EditPad Pro no, por ejemplo.

\d — Representa un dígito del 0 al 9.

\w — Representa cualquier carácter alfanumérico.

\s — Representa un espacio en blanco.

\D — Representa cualquier carácter que no sea un dígito del 0 al 9.

\W — Representa cualquier carácter no alfanumérico.

\S — Representa cualquier carácter que no sea un espacio en blanco.

\A — Representa el inicio de la cadena. No un carácter sino una posición.

\Z — Representa el final de la cadena. No un carácter sino una posición.
Docente: Ing. Mirko Manrique Ronceros
~ 42 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores

\b — Marca el inicio y el final de una palabra.

\B — Marca la posición entre dos caracteres alfanuméricos o dos noalfanuméricos.
Nota:
La
utilidad
Charmap.exe
de
Windows
permite
encontrar
los
códigos
ASCII/ANSI/UNICODE para utilizarlos en Expresiones Regulares.
Los corchetes "[]"
La función de los corchetes en el lenguaje de las expresiones regulares es representar
"clases de caracteres", o sea, agrupar caracteres en grupos o clases. Son útiles
cuando es necesario buscar uno de un grupo de caracteres. Dentro de los corchetes es
posible utilizar el guión "-" para especificar rangos de caracteres. Adicionalmente, los
metacaracteres pierden su significado y se convierten en literales cuando se
encuentran dentro de los corchetes. Por ejemplo, como vimos en la entrega anterior
"\d" nos es útil para buscar cualquier carácter que represente un dígito. Sin embargo
esta denominación no incluye el punto "." que divide la parte decimal de un número.
Para buscar cualquier carácter que representa un dígito o un punto podemos utilizar la
expresión regular "[\d.]". Como se hizo notar anteriormente, dentro de los corchetes,
el punto representa un carácter literal y no un metacaracter, por lo que no es
necesario antecederlo con la barra inversa. El único carácter que es necesario
anteceder con la barra inversa dentro de los corchetes es la propia barra inversa. La
expresión regular "[\dA-Fa-f]" nos permite encontrar dígitos hexadecimales. Los
corchetes nos permiten también encontrar palabras aún si están escritas de forma
errónea, por ejemplo, la expresión regular "expresi[oó]n" permite encontrar en un
texto la palabra "expresión" aunque se haya escrito con o sin tilde. Es necesario
aclarar que sin importar cuantos caracteres se introduzcan dentro del grupo por medio
de los corchetes, el grupo solo le dice al motor de búsqueda que encuentre un solo
carácter a la vez, es decir, que "expresi[oó]n" no encontrará "expresioon" o
"expresioón".
La barra "|"
Sirve para indicar una de varias opciones. Por ejemplo, la expresión regular "a|e"
encontrará
cualquier
"a"
o
"e"
dentro
del
texto.
La
expresión
regular
"este|oeste|norte|sur" permitirá encontrar cualquiera de los nombres de los puntos
Docente: Ing. Mirko Manrique Ronceros
~ 43 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
cardinales. La barra se utiliza comúnmente en conjunto con otros caracteres
especiales
El signo de dólar "$"
Representa el final de la cadena de caracteres o el final de la línea, si se utiliza el
modo multi-línea. No representa un carácter en especial sino una posición. Si se
utiliza la expresión regular "\.$" el motor encontrará todos los lugares donde un punto
finalice la línea, lo que es útil para avanzar entre párrafos
El acento circunflejo "^"
Este
carácter
tiene
una
doble
funcionalidad,
que
difiere
cuando
se
utiliza
individualmente y cuando se utiliza en conjunto con otros caracteres especiales. En
primer lugar su funcionalidad como carácter individual: el carácter "^" representa el
inicio de la cadena (de la misma forma que el signo de dólar "$" representa el final de
la cadena). Por tanto, si se utiliza la expresión regular "^[a-z]" el motor encontrará
todos los párrafos que den inicio con una letra minúscula. Cuando se utiliza en
conjunto con los corchetes de la siguiente forma "[^\w ]" permite encontrar cualquier
carácter que NO se encuentre dentro del grupo indicado. La expresión indicada
permite encontrar, por ejemplo, cualquier carácter que no sea alfanumérico o un
espacio, es decir, busca todos los símbolos de puntuación y demás caracteres
especiales.
La utilización en conjunto de los caracteres especiales "^" y "$" permite realizar
validaciones en forma sencilla. Por ejemplo "^\d$" permite asegurar que la cadena a
verificar representa un único dígito, "^\d\d/\d\d/\d\d\d\d$" permite validar una fecha
en formato corto, aunque no permite verificar si es una fecha válida, ya que
99/99/9999 también sería válido en este formato; la validación completa de una fecha
también es posible mediante expresiones regulares, como se ejemplifica más
adelante.
Los paréntesis"()"
De forma similar que los corchetes, los paréntesis sirven para agrupar caracteres, sin
embargo existen varias diferencias fundamentales entre los grupos establecidos por
medio de corchetes y los grupos establecidos por paréntesis:

Los caracteres especiales conservan su significado dentro de los paréntesis.
Docente: Ing. Mirko Manrique Ronceros
~ 44 ~
Universidad Nacional del Santa

Curso: Teoría de Compiladores
Los grupos establecidos con paréntesis establecen una "etiqueta" o "punto de
referencia" para el motor de búsqueda que puede ser utilizada posteriormente
como se denota más adelante.

Utilizados en conjunto con la barra "|" permite hacer búsquedas opcionales. Por
ejemplo la expresión regular "al (este|oeste|norte|sur) de" permite buscar
textos que den indicaciones por medio de puntos cardinales, mientras que la
expresión regular "este|oeste|norte|sur" encontraría "este" en la palabra
"esteban", no pudiendo cumplir con este propósito.

Utilizado en conjunto con otros caracteres
especiales que se detallan
posteriormente, ofrece funcionalidad adicional
El signo de interrogación "?"
El signo de pregunta tiene varias funciones dentro del lenguaje de las expresiones
regulares. La primera de ellas es especificar que una parte de la búsqueda es
opcional. Por ejemplo, la expresión regular "ob?scuridad" permite encontrar tanto
"oscuridad" como "obscuridad". En conjunto con los parentesis redondos permite
especificar que
un conjunto mayor
de caracteres es opcional; por
ejemplo
"Nov(\.|iembre|ember)?" permite encontrar tanto "Nov" como "Nov.", "Noviembre" y
"November". Como se mencionó anteriormente los paréntesis nos permiten establecer
un "punto de referencia" para el motor de búsqueda, sin embargo, algunas veces, no
se
desea
utilizarlos
con
este
propósito,
como
en
el
ejemplo
anterior
"Nov(\.|iembre|ember)?". En este caso el establecimiento de este punto de referencia
(que se detalla más adelante) representa una inversión inútil de recursos por parte del
motor de búsqueda. Para evitar se puede utilizar el signo de pregunta de la siguiente
forma: "Nov(?:\.|iembre|ember)?". Aunque el resultado obtenido será el mismo, el
motor de búsqueda no realizará una inversión inútil de recursos en este grupo, sino
que lo ignorará. Cuando no sea necesario reutilizar el grupo, es aconsejable utilizar
este formato. De forma similar, es posible utilizar el signo de pregunta con otro
significado: Los paréntesis definen grupos "anónimos", sin embargo el signo de
pregunta en conjunto con los paréntesis triangulares "<>" permite "nombrar" estos
grupos de la siguiente forma: "^(?<Día>\d\d)/(?<Mes>\d\d)/(?<Año>\d\d\d\d)$";
Con lo cual se le especifica al motor de búsqueda que los primeros dos dígitos
encontrados llevarán la etiqueta "Día", los segundos la etiqueta "Mes" y los últimos
cuatro dígitos llevarán la etiqueta "Año".
Docente: Ing. Mirko Manrique Ronceros
~ 45 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Nota: a pesar de la complejidad y flexibilidad dada por los caracteres especiales
estudiados hasta ahora, en su mayoría nos permiten encontrar solamente un caractér
a la vez, o un grupo de caracteres a la vez. Los metacaracteres enumerados en
adelante permiten establecer repeticiones
Las llaves "{}"
Comúnmente las llaves son caracteres literales cuando se utilizan por separado en
una expresión regular. Para que adquieran su función de metacaracteres es necesario
que encierren uno o varios números separados por coma y que estén colocados a la
derecha de otra expresión regular de la siguiente forma: "\d{2}" Esta expresión le
dice al motor de búsqueda que encuentre dos dígitos contiguos. Utilizando esta
fórmula podríamos convertir el ejemplo "^\d\d/\d\d/\d\d\d\d$" que servía para
validar un formato de fecha en "^\d{2}/\d{2}/\d{4}$" para una mayor claridad en la
lectura de la expresión.
Nota: aunque esta forma de encontrar elementos repetidos es muy útil, algunas veces
no se conoce con claridad cuantas veces se repite lo que se busca o su grado de
repetición es variable. En estos casos los siguientes metacaracteres son útiles.
El asterisco "*"
El asterisco sirve para encontrar algo que se encuentra repetido 0 o más veces. Por
ejemplo, utilizando la expresión "[a-zA-Z]\d*" será posible encontrar tanto "H" como
"H1", "H01", "H100" y "H1000", es decir, una letra seguida de un número indefinido
de dígitos. Es necesario tener cuidado con el comportamiento del asterisco, ya que
este por defecto trata de encontrar la mayor cantidad posible de caracteres que
correspondan con el patrón que se busca. De esta forma si se utiliza "\(.*\)" para
encontrar cualquier cadena que se encuentre entre paréntesis y se lo aplica sobre el
texto "Ver (Fig. 1) y (Fig. 2)" se esperaría que el motor de búsqueda encuentre los
textos "(Fig. 1)" y "(Fig. 2)", sin embargo, debido a esta característica, en su lugar
encontrará el texto "(Fig. 1) y (Fig. 2)". Esto sucede porque el asterisco le dice al
motor de búsqueda que llene todos los espacios posibles entre dos paréntesis. Para
obtener el resultado deseado se debe utilizar el asterisco en conjunto con el signo de
pregunta de la siguiente forma: "\(.*?\)" Esto es equivalente a decirle al motor de
búsqueda que "Encuentre un paréntesis de apertura y luego encuentre cualquier
carácter repetido hasta que encuentre un paréntesis de cierre”
Docente: Ing. Mirko Manrique Ronceros
~ 46 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
El signo de suma "+"
Se utiliza para encontrar una cadena que se encuentre repetida 1 o más veces. A
diferencia del asterisco, la expresión "[a-zA-Z]\d+" encontrará "H1" pero no
encontrará "H". También es posible utilizar este metacaracter en conjunto con el signo
de pregunta para limitar hasta donde se efectúa la repetición
Grupos anónimos
Los grupos anónimos se establecen cada vez que se encierra una expresión regular en
paréntesis, por lo que la expresión "<([a-zA-Z]\w*?)>" define un grupo anónimo que
tendrá como resultado que el motor de búsqueda almacenará una referencia al texto
que corresponda a la expresión encerrada entre los paréntesis.
La forma más inmediata de utilizar los grupos que se definen es dentro de la misma
expresión regular, lo cual se realiza utilizando la barra inversa "\" seguida del número
del grupo al que se desea hacer referencia de la siguiente forma: "<([a-zAZ]\w*?)>.*?</\1>" Esta expresión regular encontrará tanto la cadena "Esta" como la
cadena "prueba" en el texto "Esta es una prueba" a pesar de que la expresión no
contiene los literales "font" y "B".
Docente: Ing. Mirko Manrique Ronceros
~ 47 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
AUTOMATAS
Autómata del griego automatos (αὐτόματος) que significa espontáneo o con
movimiento propio, puede referirse a:

Autómata programable: Equipo electrónico programable en lenguaje no
informático y diseñado para controlar, en tiempo real y en ambiente industrial,
procesos secuenciales.

Teoría de autómatas: Estudio matemático de máquinas abstractas. (p.e.
Autómata finito, autómata con pila)

Autómata (mecánico): Máquina que imita la figura y los movimientos de un
ser animado.

Robot: Máquina o ingenio electrónico programable, capaz de manipular objetos
y realizar operaciones antes reservadas solo a las personas.
Teoría de autómatas
La teoría de autómatas es una rama de las ciencias de la computación que estudia
matemáticamente máquinas abstractas y problemas que éstas son capaces de
resolver. La teoría de autómatas esta estrechamente relacionada con la teoría del
lenguaje formal ya que los autómatas son clasificados a menudo por la clase de
lenguajes formales que son capaces de reconocer.
Un autómata es un modelo matemático para una máquina de estado finita (FSM sus
siglas en inglés). Una FSM es una máquina que, dada una entrada de símbolos, "salta"
a través de una serie de estados de acuerdo a una función de transición (que puede
ser expresada como una tabla). En la variedad común "Mealy" de FSMs, esta función
de transición dice al autómata a que estado cambiar dados un determinado estado y
símbolo.
La entrada es leída símbolo por símbolo, hasta que es "consumida" completamente
(piense en esta como una cinta con una palabra escrita en ella, que es leída por por
una cabeza lectora del autómata; la cabeza se mueve a lo largo de la cinta, leyendo
un símbolo a la vez) una vez la entrada se ha agotado, el autómata se detiene.
Dependiendo del estado en el que el autómata para se dice que este a aceptado o
rechazado la entrada. Si este termina en el estado "acepta", el autómata acepta la
palabra. Si lo hace en el estado "rechaza", el autómata rechazó la palabra, el conjunto
de todas las palabras aceptadas por el autómata constituyen el lenguaje aceptado por
el mismo.
Docente: Ing. Mirko Manrique Ronceros
~ 48 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
HERRAMIENTAS PARA COMPILADORES
Un analizador léxico también es conocido como escáner; pues su funcionalidad es la
de analizar el lexema de las palabras o cadenas de caracteres sobre un patrón
definido.
Es decir; El proceso de análisis léxico se refiere al trabajo que realiza el scanner con
relación al proceso de compilación. El scanner representa una interfaz entre el
programa fuente y el analizador sintáctico o parser. El scanner, a través del examen
carácter por carácter del texto, separa el programa fuente en piezas llamadas tokens,
los cuales representan los nombres de las variables, operadores, etiquetas, y todo lo
que comprende el programa fuente
Un analizador de léxico tiene como función principal el tomar secuencias de caracteres
o símbolos del alfabeto del lenguaje y ubicarlas dentro de categorías, conocidas como
unidades de léxico. Las unidades de léxico son empleadas por el analizador gramatical
para determinar si lo escrito en el programa fuente es correcto o no gramaticalmente.
Algunas de las unidades de léxico no son empleadas por el analizador gramatical sino
que son descartadas o filtradas. Tal es el caso de los comentarios, que documentan el
programa pero que no tienen un uso gramatical, o los espacios en blanco, que sirven
para dar legibilidad a lo escrito.
Algunos generadores de Analizadores Léxico…
LEX
Código generado: C.
FLEX Código generado: C++.
ZLEX Código generado: C.,
Soporta códigos de caracteres de 16 bits.
JAX
Código generado: Java.
No soporta entornos, está basado en expresiones regulares.
No soporta Unicode.
JLEX Código generado: Java.
Similar a lex.
Diseñado para ser usado junto con CUP.
JFLEX Código generado: Java.
Diseñado para ser usado junto con CUP.
Docente: Ing. Mirko Manrique Ronceros
~ 49 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
LEX:
Recibe la especificación de las expresiones regulares de los patrones que representan
a los tokens del lenguaje y las acciones a tomar cuando los detecte.

Genera los diagramas de transición de estados en código C, C ++, o Java
generalmente.

Ventajas: Comodidad de Desarrollo.

Desventajas:
1. El mantenimiento del código generado resulta complicado.
2. La eficiencia del código generado depende del generador.
Parte de un conjunto de reglas léxicas (expresiones regulares) y produce un programa
(yylex) que reconoce las cadenas que cumplen dichas reglas.
1. Yylex es la implementación del Autómata Finito Determinista.
FLEX:
Flex es una herramienta para generar escáneres: programas que reconocen patrones
léxicos en un texto. flex lee los ficheros de entrada dados, o la entrada estándar si no
se le ha indicado ningún nombre de fichero, con la descripción de un escáner a
generar. La descripción se encuentra en forma de parejas de expresiones regulares y
código C, denominadas reglas. flex genera como salida un fichero fuente en C,
`lex.yy.c', que define una rutina `yylex()'. Este fichero se compila y se enlaza con la
librería `-lfl' para producir un ejecutable. Cuando se arranca el fichero ejecutable,
este analiza su entrada en busca de casos de las expresiones regulares. Siempre que
encuentra uno, ejecuta el código C correspondiente
El principal objetivo de diseño de flex es que genere analizadores de alto rendimiento.
Este ha sido optimizado para comportarse bien con conjuntos grandes de reglas.
Aparte de los efectos sobre la velocidad del analizador con las opciones de compresión
de tablas `-C' hay un número de opciones/acciones que degradan el rendimiento.
Flex ofrece dos maneras distintas de generar analizadores para usar con C++. La
primera manera es simplemente compilar un analizador generado por flex usando un
compilador de C++ en lugar de un compilador de C. No debería encontrarse ante
ningún error de compilación Puede entonces usar código C++ en sus acciones de las
reglas en lugar de código C. Fíjese que la fuente de entrada por defecto para su
Docente: Ing. Mirko Manrique Ronceros
~ 50 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
analizador permanece como yyin, y la repetición por defecto se hace aún a yyout.
Ambos permanecen como variables `FILE *' y no como flujos de C++.
Flex es una reescritura de la herramienta lex del Unix de AT&T (aunque las dos
implementaciones
no
comparten
ningún
código),
con
algunas
extensiones
e
incompatibilidades, de las que ambas conciernen a aquellos que desean escribir
analizadores aceptables por cualquier implementación. Flex sigue completamente la
especificación POSIX de lex, excepto que cuando se utiliza `%pointer' (por defecto),
una llamada a `unput()' destruye el contenido de yytext, que va en contra de la
especificación POSIX.
http://ditec.um.es/~aflores/dile/flex/flex-es_toc.html#TOC1
JAX:
Jax es un compilador léxico creado en lenguaje Java, que genera un escáner a partir
de expresiones regulares que existen por defecto en un archivo de java.
Jax procesa estas expresiones regulares y genera un ficher Java que pueda ser
compilado por Java y así crear el escaner.
Los escaners generados por Jax tienen entradas de búfer de tamaño arbitrario, y es al
menos más conveniente para crear las tablas de tokens, Jax utiliza solo 7 bits de
caracteres ASCII, y no permite código Unario.
http://www.cs.princeton.edu/~ejberk/JavaLex/JavaLex.html
Uno de los aspectos fundamentales que tienen los lenguajes Lex y Yacc es que son
parte importante de un compilador. Pues la unión de estos dos generan un
compilador, claro cada uno de ellos aportando su propio diseño y su forma de ejecutar
sus procesos.
Tanto el analizador léxico como el sintáctico pueden ser escritos en cualquier
lenguaje de programación. A pesar de la habilidad de tales lenguajes de propósito
general como C, lex y yacc son más flexibles y mucho menos complejos de usar.
Docente: Ing. Mirko Manrique Ronceros
~ 51 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
LENGUAJE LEX
¿QUÉ ES LEX?
Lex es un generador de analizadores léxicos. Cada vez que Lex encuentra un
lexema que viene definido por una expresión regular, se ejecutan las acciones
(escritas en C) que van al lado de la definición de dicha expresión.
Cuando se emplea el término Lex, se mencionan dos posibles significados:
a. Una notación para especificar las características lexicográficas de un
lenguaje de programación,
b. Un traductor de especificaciones lexicográficas.
Esta misma dualidad también es de aplicación al término Yacc.
Lex crea yylex, una variable que contendrá un número, el cual se corresponde
con el token de cada expresión regular. También, instancia una variable global yytext,
que contiene el lexema que acaba de reconocer.
Así, por ejemplo, para el siguiente código fuente de entrada:
%%expresión1 {acción1}expresión2
{acción2}...
...expresiónn
{acciónn}%%
Lex genera un programa en C (generalmente denominado lex.yy.c) que incluye,
entre otras, la función yylex():
Int yylex(){ while(!eof())
switch(...)
{
{
case -1: ... ; break;
case 0: ... ; break;
case 1: {acción1}; break;
case n: {acciónn}; break;
}
...
...
...
}...}
Esta función recorre el texto de entrada. Al descubrir algún lexema que se
corresponde con alguna expresión regular, construye el token, y realiza la acción
correspondiente. Cuando se alcanza el fin de fichero, devolverá -1. La función yylex()
podrá ser invocada desde cualquier lugar del programa.
Cómo escribir expresiones regulares en Lex
Docente: Ing. Mirko Manrique Ronceros
~ 52 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Deberán cumplir los siguientes requisitos:

Las expresiones regulares (ER, de aquí en adelante) han de aparecer en la
primera columna.

Alfabeto de entrada: Caracteres ASCII 0 al 127.

Concatenación: Sin carácter especial, se ponen los caracteres juntos.

Caracteres normales: Se representan a ellos mismos.

Caracteres especiales: Se les pone la barra '\' delante. Éstos son:
*+?|[]()"\.{}^$/<>

Caracteres especiales dentro de los corchetes ( '[' y ']' ): - \ ^
Las equivalencias entre ER normales y las que se usan en Lex se muestran con
ejemplos en esta tabla:
Caracteres
Ejemplo
Significado
Concatenación
Xy
El patrón consiste en x seguido de y.
Unión
x|y
El patrón consiste en x o en y.
Repetición
x*
El patrón consiste en x repetido cero a más
veces.
Clases
de [0-9]
caracteres
Alternancia
indicado,
de
en
caracteres
este
caso
en
el
rango
0|1|2|...|9.
Más de un rango se puede especificar, como
por ejemplo: [0-9A-Za-z] para caracteres
alfanuméricos.
Operador
[^0-9]
negación
El primer carácter en una clase de caracteres
deberá ser ^ para indicar el complemento del
conjunto
de
caracteres
especificado.
Así,
[^0-9] especifica cualquier carácter que no
sea un dígito.
Carácter
.
Con un único carácter, excepto \n.
x?
Cero o una ocurrencia de x.
arbitrario
Repetición
única
Repetición
no x+
Una o más ocurrencias de x.
nula
Docente: Ing. Mirko Manrique Ronceros
~ 53 ~
Universidad Nacional del Santa
Repetición
Curso: Teoría de Compiladores
x{n,m}
x repetido entre n y m veces.
especificada
Comienzo
de ^x
Unifica x sólo al comienzo de una línea
línea
Fin de línea
x$
Unifica x sólo al final de una línea
Sensibilidad
al ab/cd
Unifica ab, pero sólo seguido de bc.
contexto
(operador "look
ahead")
Cadenas
de "x"
Cuando x tenga un significado especial.
literales
Caracteres
\x
Cuando x es un operador que se representa a
literales
él mismo. También para el caso de \n, \t,
etc.
Definiciones
{nombre_var}
Pueden definirse subpatrones. Esto significa
incluir
el
patrón
predefinido
llamado
nombre_var.
Definiciones en Lex
La sección de definiciones permite predefinir cadenas que serán útiles en la
sección de las reglas. Por ejemplo:
comentario"//".*limitador
[ \t\n]espblanco
{limitador}+letramay
[A-Z]letramin
{letramay}|{letramin}carascii
[a-z]letra
[^\"\n]caresc
\\n|\\\"digito
[0-9]variable
{letramin}({letramin}|{digito})*entero
{digito}+texto \"{(carascii}|{caresc})*\"
Cada regla está compuesta de un nombre que se define en la parte izquierda, y
su definición se coloca en la derecha. Así, podemos definir comentario como // (con la
barra puesto que es un carácter especial), seguido por un número arbitrario de
caracteres excepto el de fin de línea. Un limitador será un espacio, tabulador o fin de
línea, y un espblanco será uno o más limitadores. Nótese que la definición de
espblanco usa la definición anterior de limitador.
Reglas para eliminar ambigüedades
Docente: Ing. Mirko Manrique Ronceros
~ 54 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Se producen cuando varias ER's son aplicables a un mismo lexema reconocido. Lex
seleccionará siempre la ocurrencia más larga posible. Si dos ocurrencias tienen la
misma longitud, tomará la primera.
Formalmente, podemos definir a lex como una herramienta para construir
analizadores léxicos o "lexers". Un lexer lee de un flujo de entrada cualquiera, y la
divide en unidades léxicas (la tokeniza), para ser procesada por otro programa o
como producto final.
La entrada es tomada de yyin, que por defecto su valor es stdin, es decir, la pantalla o
terminal, pero este valor puede ser modificado por cualquier apuntador a un archivo.
También es posible leer la entrada desde un arreglo de caracteres u otros
medios, para cual es necesario implementar algunas funciones de lex mismas que
definiremos en la última parte de esta sección (Agregar Funcionalidad).
Expresiones regulares usadas en lex
Para poder crear expresiones regulares y patrones para las reglas, es necesario
saber que la concatenación de expresiones se logra simplemente juntando dos
expresiones, sin dejar espacio entre ellas y que es bueno declarar una expresión muy
compleja por partes como definiciones, y así evitar tener errores difíciles de encontrar
y corregir.
A continuación una lista de las expresiones regulares mas usadas en lex.
Ops
Ejemplo
Explicación
[]
[a-z]
Una
clase
de
Caracteres,
coincide
con
un
carácter
perteneciente a la clase, pueden usarse rangos, como en el
ejemplo, cualquier carácter, excepto aquellos especiales o de
control son tomados literalmente, en el caso de los que no,
pueden usarse secuencias de escape como las de C, \t, \n
etcétera.
Si su primer carácter es un "^", entonces coincidirá con
cualquier carácter fuera de la clase.
*
[ \n\t]*
Todas las cadenas que se puedan formar, se puede decir que
este operador indica que se va a coincidir con cadenas
formadas por ninguna o varias apariciones del patrón que lo
Docente: Ing. Mirko Manrique Ronceros
~ 55 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
antecede.
El ejemplo coincide con cualquier combinación de símbolos
usados para separar, el espacio, retorno y tabulador.
+
[0-9]+
Todas las cadenas que se puedan formar, excepto cadenas
vacías. En el ejemplo se aceptan a todos los números
naturales y al cero.
.
.+
Este es una expresión regular que coincide con cualquier
entrada excepto el retorno de carro ("\n"). El ejemplo acepta
cualquier cadena no vacía.
{}
a{3,6}
Indica un rango de repetición cuando contiene dos números
separados por comas, como en el ejemplo, la cadena aceptada
será aquella con longitud 3, 4, 5 o 6 formada por el carácter
'a'.
Indica una repetición fija cuando contiene un solo numero, por
ejemplo, a{5}, aceptaría cualquier cadena formada por 5 a's
sucesivas.
En caso de contener un nombre, indica una sustitución por una
declaración
en
la
sección
de
declaraciones
(Revisar
el
ejemplo1).
?
-?[0-9]+
Indica que el patrón que lo antecede es opcional, es decir,
puede existir o no. En el ejemplo, el patrón coincide con todos
los números enteros, positivos o negativos por igual, ya que el
signo es opcional.
|
(-|+|~)?[0- Este hace coincidir, al patrón que lo precede o lo antecede y
9]+
puede usarse consecutivamente. En el ejemplo tenemos un
patrón que coincidirá con un entero positivo, negativo o con
signo de complemento.
""
"bye"
Las cadenas encerradas entre " y " son aceptadas literalmente,
es decir tal como aparecen dentro de las comillas, para incluir
caracteres de control o no imprimibles, pueden usarse dentro
de ellas secuencias de escape de C. En el ejemplo la única
cadena que coincide es 'bye'.
\
\.
Indica a lex que el carácter a continuación será tomado
literalmente, como una secuencia de escape, este funciona
para todos los caracteres reservados para lex y para C por
Docente: Ing. Mirko Manrique Ronceros
~ 56 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
igual. En el ejemplo, el patrón coincide solo con el carácter "."
(punto), en lugar de coincidir con cualquier carácter, como
seria el casi sin el uso de "\".
<<EOF>> [a-z]
Solo en flex, este patrón coincide con el fin de archivo.
Ampliación de las expresiones regulares
Las expresiones regulares (propiamente dichas, en un sentido estricto), tal y
como se estudian en la teoría de lenguajes para especificar los lenguajes regulares,
están constituidas por símbolos de un alfabeto Σ, relacionados mediante los operadores
binarios alternativa (|) y concatenación (·) y el operador unitario estrella (*); en la
escritura de una expresión regular también se pueden emplear paréntesis para precisar
el orden de aplicación de los operadores. El asterisco de la operación estrella suele
colocarse como exponente de la parte de la expresión regular afectada.
La precedencia de los operadores es la definida por la siguiente jerarquía,
relacionada de mayor a menor precedencia:
1. operaciones entre paréntesis
2. operador estrella
3. operador concatenación
4. operador alternativa
Así, por ejemplo, son expresiones regulares definidas sobre el alfabeto Σ = {a, b}
b·a·a|b·b
a*·(b|b·a)
La primera denota el lenguaje regular formado por dos palabras {baa, bb} y la
segunda denota el lenguaje regular de infinitas palabras {b, ba, ab, aba, aab, aaba,  
}.
Entre los símbolos que aparecen en una expresión regular cabe distinguir los
caracteres y los metacaracteres; los caracteres son los símbolos que pertenecen al
alfabeto sobre el que está definida la expresión regular; los metacaracteres son los
símbolos que no pertenecen a ese alfabeto: los operadores y los paréntesis.
En la escritura de las expresiones regulares el punto representativo de la
concatenación entre símbolos del alfabeto suele suprimirse; de acuerdo con esta
notación simplificada, las anteriores expresiones suelen escribirse así:
baa|bb
a*(b|ba)
Docente: Ing. Mirko Manrique Ronceros
~ 57 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Dado que el espacio en blanco no es un símbolo perteneciente al alfabeto Σ sobre
el que están definidas las expresiones regulares anteriores, también podrían escribirse
(sin ocasionar confusión y con la pretensión de favorecer la legibilidad) de esta manera:
baa | bb
a* ( b | ba )
En una especificación Lex se incluyen expresiones regulares, pero escritas con
una notación que es una ampliación de la notación empleada en la definición (en
sentido estricto) anterior. Esta ampliación tiene como principales objetivos:
- hacer más cómoda y escueta la escritura de las expresiones regulares,
- distinguir de manera precisa los caracteres del alfabeto y los metacaracteres
empleados en la escritura de las expresiones regulares.
Sea el alfabeto Σ = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}, la expresión regular que denota
las palabras de longitud uno es:
0|1|2|3|4|5|6|7|8|9
Con la notación ampliada de las expresiones regulares Lex (empleando unos
nuevos metacaracteres: el guión y los corchetes de abrir y de cerrar), esa misma
expresión puede escribirse así:
[0-9]
Las expresiones regulares de una especificación Lex han de procesarse mediante
un programa y, por ello, han de estar grabadas en un fichero de tipo texto. En estas
condiciones no resulta adecuado el convenio según el cual la operación estrella se
escribe en forma de exponente; por ejemplo, la expresión regular
ab*
Quedaría grabada en el fichero mediante la secuencia de tres caracteres
consecutivos ab*. Si Σ = {+, -, *, /} es el alfabeto sobre el que definen las expresiones
regulares, ¿qué lenguaje denota la expresión regular +* grabada como una secuencia
de dos caracteres? La respuesta depende de si el asterisco se considera como carácter
del alfabeto o como operador (metacarácter).
Las expresiones regulares empleadas en las especificaciones Lex no tienen
conceptualmente ninguna diferencia con las expresiones regulares (en sentido
estricto) que definen los lenguajes formales regulares; lo único que aportan son
modificaciones en la notación empleada en la escritura de las expresiones en forma de
secuencias de caracteres consecutivos susceptibles de grabarse en un fichero de tipo
texto. Esta notación ampliada alcanza cierta dificultad por las siguientes causas:

Para facilitar y acortar la escritura de las expresiones se introducen bastantes
metacaracteres que se intercalan con los caracteres
Docente: Ing. Mirko Manrique Ronceros
~ 58 ~
Universidad Nacional del Santa

Curso: Teoría de Compiladores
Es habitual que cualquier carácter del alfabeto ASCII forme parte del alfabeto
sobre el que se definen las expresiones regulares y que, por lo tanto, pueda
aparecer en ellas (incluso el espacio En blanco, el tabulador o el fin de línea).

Se precisa la definición de convenios para distinguir entre los caracteres y los
metacaracteres.
En lo que sigue se emplea la notación  para indicar que lo que se pone a
continuación de esos símbolos es la descripción del conjunto de palabras denotadas por
la expresión regular que les precede.
Variables y Funciones, que se usan dentro de un programa generado por lex
FILE *yyin
Este es un apuntador declarado globalmente que apunta al lugar de donde se
van a leer los datos, por ser un file pointer, este solo puede leer de flujos como
archivos, para leer de una cadena es necesario reimplementar el macro input() como
se vera mas adelante.
FILE *yyout
Este es el lugar al que se escriben por default todos los mensajes, al igual que
yyin esta declarado globalmente y es un apuntador.
int input(void)
El objetivo de esta Macro es alimentar a yylex() carácter por carácter, devuelve
el siguiente carácter de la entrada, la intención más común para modificar esta
función, es cambiar el origen de la entrada de manera mas flexible que con yyin, ya
que no solo es posible leer de otro archivo, sino que también es posible leer el flujo
para parsear una cadena cualquiera, o un grupo de cadenas como una línea de
comandos.
Para reimplementar esta macro, es necesario primero eliminarla del archivo por lo que
es necesario incluir un comando del preprocesador de C el sección de declaraciones :
%{
#undef input
%}
y en la parte de subrutinas, implementar nuestro nuevo input() con el prototipo
mostrado anteriormente.
void unput(int)
Docente: Ing. Mirko Manrique Ronceros
~ 59 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
El objetivo de esta macro, es regresar un carácter a la entrada de datos, es útil
para yylex() tener una de estas, ya que para identificar un patrón puede ser necesario
saber que carácter es el que sigue. La intención de reimplementar esta es
complementar el uso de la reimplementacion de input(), ya que input() y unput()
deben ser congruentes entre si.
Antes de reimplementar esta, también es necesario eliminarla antes usando una
instrucción del preprocesador de C:
%{
#undef unput
%}
int yywrap(void)
Esta función, es auxiliar en el manejo de condiciones de final de archivo, su
misión es proporcionarle al programador la posibilidad de hacer algo con estas
condiciones, como continuar leyendo pero desde otro archivo etcétera.
int yylex(void)
Esta función, es casi totalmente implementada por el usuario en la sección de
reglas, donde como ya vimos, puede agregarse código encerrado entre %{ y %} así
como en las reglas mismas.
int yyleng;
Contiene la longitud del token leido, su valor es equivalente a yyleng =
strlen(yytext);.
char *yytext;
Contiene el token que acaba de ser reconocido, su uso es principalmente dentro
de las reglas, donde es común hacer modificaciones al token que acaba de ser leído o
usarlo con algún otro fin. En el ejemplo 1 este token es usado para dar echo en la
pantalla.
void output(int);
Esta macro, escribe su argumento en yyout.
void yyinput(void);
Es una interfaz para la macro input().
void yyunput(int);
Es una interfaz para la macro unput().
void yyoutput(int);
Docente: Ing. Mirko Manrique Ronceros
~ 60 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Es una interfaz para la macro output().
Partes del un programa Lex
Un programa Lex consta de tres secciones:
<declaraciones>
%%
<reglas de traducción>
%%
<procedimientos auxiliares>
La sección de declaraciones incluye declaraciones de variables, constantes y
definiciones regulares. Las definiciones regulares son sentencias usadas como
componentes de las expresiones regulares que aparecen en las reglas.
Las reglas de traducción de un programa Lex son sentencias de la forma:
p1 { acción1 }
p2 { acción2 }
...
...
pn { acciónn }
donde cada pi es una expresión regular y cada accióni es un fragmento de programa,
describiendo qué acción debe realizar el analizador léxico cuando el patrón pi se
corresponde con un lexema. En Lex, las acciones están escritas en C.
La tercera sección contiene cualesquiera procedimientos auxiliares que sean
requeridos por las acciones. Alternativamente, estos procedimientos pueden ser
compilados separadamente y montados junto con el analizador léxico.
Un analizador léxico creado por Lex funciona en concierto con un analizador
sintáctico de la siguiente manera. Cuando es activado por el analizador sintáctico, el
analizador léxico comienza leyendo de su entrada un carácter a la vez, hasta que
encuentre el prefijo más largo de la entrada que ha correspondido con una de las
expresiones regulares pi. Entonces, ejecuta accióni, que típicamente devolverá el
control al parser. Pero, si no lo hace, entonces el analizador léxico procede a buscar
más lexemas, hasta que una acción contenga una sentencia return o se lea el fichero
completo. La búsqueda repetida de lexemas hasta una devolución explícita del control
permite que el analizador léxico procese los espacios en blanco y comentarios
convenientemente. El analizador léxico devuelve un entero, que representa el token,
al analizador sintáctico. Para pasar un valor de atributo con información sobre el
lexema, se puede usar una variable global llamada yylval. Esto se hace cuando se use
Yacc como generador del analizador sintáctico.
Docente: Ing. Mirko Manrique Ronceros
~ 61 ~
Universidad Nacional del Santa
Los
analizadores
léxicos,
Curso: Teoría de Compiladores
para
ciertas
construcciones
de
lenguajes
de
programación, necesitan ver adelantadamente más allá del final de un lexema antes
de que puedan determinar un token con certeza. En Lex, se puede escribir un patrón
de la forma r1/r2, donde r1 y r2 son expresiones regulares, que significa que una
cadena se corresponde con r1, pero sólo si está seguida por una cadena que se
corresponde con r2. La expresión regular r2, después del operador lookahead "/",
indica el contexto derecho para una correspondencia; se usa únicamente para
restringir una correspondencia, no para ser parte de la correspondencia.
Programación de analizadores mediante LEX
Lex suele ser usado según la siguiente figura:
Primero, se prepara una especificación de un analizador léxico creando un
programa contenido, por ejemplo en el fichero prog.l, en lenguaje Lex. Entonces,
prog.l se pasa a través del compilador Lex para producir un programa en C, que por
defecto se denomina lex.yy.c en el sistema operativo UNIX. Éste consiste en una
representación tabular de un diagrama de transición construido a partir de las
expresiones regulares de prog.l, junto con una rutina estándar que usa la tabla de
reconocimiento de lexemas. Las acciones asociadas con expresiones regulares en
prog.l son trozos de código C, y son transcritas directamente a lex.yy.c. Finalmente,
lex.yy.c se pasa a través del compilador C para producir un programa objeto, que por
defecto se llama a.out, el cual es el analizador léxico que transforma una entrada en
una secuencia de tokens.
Docente: Ing. Mirko Manrique Ronceros
~ 62 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
RECUPERACIÓN DE ERRORES LEXICOGRÁFICOS
Los programas pueden contener diversos tipos de errores, que pueden ser:

Errores lexicográficos: Que veremos a continuación.

Errores sintácticos: Por ejemplo, una expresión aritmética con mayor numero
de paréntesis de apertura que de cierre.

Errores semánticas: Por ejemplo, la aplicación de un operador a un tipo de
datos incompatible con el mismo.

Errores lógicos: Por ejemplo, un bucle sin final.
Cuando se detecta un error, un compilador puede detenerse en ese punto e
informar al usuario, o bien desechar una serie de caracteres del texto fuente y
continuar con el análisis, dando al final una lista completa de todos los errores
detectados. En ciertas ocasiones es incluso posible que el compilador corrija el error,
haciendo una interpretación coherente de los caracteres leídos. En estos casos, el
compilador emite una advertencia, indicando la suposición que ha tomado, y continúa
el proceso sin afectar a las sucesivas fases de compilación.
Los errores lexicográficos se producen cuando el analizador no es capaz de generar un
token tras leer una determinada secuencia de caracteres. En general, puede decirse
que los errores lexicográficos son a los lenguajes de programación lo que las faltas de
ortografía a los lenguajes naturales. Las siguientes situaciones producen con
frecuencia la aparición de errores lexicográficos:
1. Lectura de un carácter que no pertenece al vocabulario terminal previsto para
el autómata. Lo más normal en este caso es que el autómata ignore estos
caracteres extraños y continue el proceso normalmente. Por ejemplo, pueden
dar error en la fase de análisis lexicográfico la inclusión de caracteres de control
de la impresora en el programa fuente para facilitar su listado.
2. Omisión de un carácter. Por ejemplo, si se ha escrito ELS en lugar de ELSE.
3. Se ha introducido un nuevo caracter. Por ejemplo, si escribimos ELSSE en lugar
de ELSE.
4. Han sido permutados dos caracteres en el token analizado. Por ejemplo, si
escribiéramos ESLE en lugar de ELSE.
5. Un carácter ha sido cambiado. Por ejemplo, si se escribiera ELZE en vez de
ELSE.
Las técnicas de recuperación de errores lexicográficos se basan, en general, en la
obtención de los distintos sinónimos de una determinada cadena que hemos detectado
Docente: Ing. Mirko Manrique Ronceros
~ 63 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
como errónea. Por otra parte, el analizador sintáctico es capaz en muchos casos de
avisar al analizador lexicográfico de cuál es el token que espera que éste lea.
Análogamente, podemos incluir rutinas para los demás casos. Por ejemplo, si el
analizador lee el lexema ESLE, y no puede construir un token correcto para él mismo,
procedería a generar los sinónimos por intercambio de caracteres (es decir, SELE,
ELSE o ESEL) y comprobaría si alguno de ellos es reconocible. En caso afirmativo,
genera el token correspondiente y advierte al usuario del posible error y de su
interpretación automática, continuando con el proceso.
Todos los procedimientos para la recuperación de errores lexicográficos son en la
práctica métodos específicos, y muy dependientes del lenguaje que se pretende
compilar.
Reglas de Lex
Esta sección también puede incluir código de C encerrado por %{ y %}, que será
copiado dentro de la función yylex(), su alcance es local dentro de la misma función.
Las reglas de lex, tienen el siguiente formato :
<Expresión regular><Al menos un espacio>{Código en C}
En el ejemplo podemos ver que :
"bye"
{bye();return 0;}
"quit"
{bye();return 0;}
"resume"
{bye();return 0;}
{Palabra}
{printf("Se leyó la palabra : %s", yytext);palabra++;}
{Numero}
{printf("Se leyó el numero : %d", atoi(yytext));numero++;}
. printf("%s",yytext[0]);
Como ya vimos en la segunda columna se escriben acciones en C a realizar
cada que se acepta una cadena con ese patrón, misma que es almacenada en un
array apuntado por yytext, podemos ver que las acciones están encerradas entre "{"
y "}" lo que indica que se incluye más de un statement de C por regla, el contra
ejemplo es la ultima regla, que reconoce cualquier carácter y lo imprime a la pantalla
mediante el uso de printf().
Entonces, podemos decir que una regla de lex esta formada por una expresión
regular y la acción correspondiente, típicamente encerrada entre "{" y "}".
Docente: Ing. Mirko Manrique Ronceros
~ 64 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Notas complementarias sobre Lex

Cada vez que se realice una de las acciones, la variable char *yytext contendrá
el lexema reconocido.

La variable int yyleng contiene la longitud del lexema reconocido.

Entrada y salida: FILE *yyin, *yyout. Por defecto, se usan los predefinidos en C.

Cuando Lex reconoce el carácter de fin de fichero, llama a la función int
yywrap(), que por defecto devuelve 1. Si devuelve 0, significará que está
disponible una entrada anterior, con lo cuál aún no se habrá terminado la
lectura.

Contextos: Permiten especificar cúando se usarán ciertas reglas. Veremos
mediante un ejemplo de eliminación de comentarios cómo se usan los
contextos:
%start COMENTARIO%%\/\* {BEGIN COMENTARIO;} /* activa
COMENTARIO */<COMENTARIO>\*\/ {BEGIN 0;} /* desactiva
todos */<COMENTARIO>. ; /* nothing to
do! ;-P */[a-zA-Z][a-z0-9]+ ;...%%
Ejemplos en lex
EJEMPLO:
A continuación se presenta un ejemplo que ilustra de manera general el uso de lex
para reconocer patrones de expresiones regulares básicas, que reconoce cualquier
numero entero y cualquier palabra formada por letras mayúsculas de la "a" a la "z",
sin importar si son mayúsculas o minúsculas.
Download
%{
#include
int palabra=0, numero=0;
%}
Numero -?[0-9]+
Palabra [a-zA-Z]+
%%
"bye"
{bye();return 0;}
"quit"
{bye();return 0;}
"resume"
{bye();return 0;}
Docente: Ing. Mirko Manrique Ronceros
~ 65 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
{Palabra}
{printf("Se leyó la palabra : %s", yytext);palabra++;}
{Numero}
{printf("Se leyó el numero : %d", atoi(yytext));numero++;}
. printf("%s",yytext[0]);
%%
main(){
printf("ejem1.l\nEste
ejemplo,
distingue
entre
un
numero
entero
y
palabras.\n Introduzca bye, quit o resume para terminar.\n");
yylex();
}
bye(){
printf("Se
leyeron
%d
entradas,
reconocieron\n%d\tEnteros\ny\n%d\tPalabras.\n",
de
las
cuales
(palabra+numero),
se
numero,
palabra);
}
En este ejemplo, una de las primeras cosas a notar, son las dos líneas "%%" que
sirven como separadores para las tres secciones de una especificación lex, la primera,
la de definiciones, sirve para definir cosas que se van a usar en el programa
resultante o en la misma especificación:
%{
#include
int palabra=0, numero=0;
%}
Numero -?[0-9]+
Palabra [a-zA-Z]+
Podemos ver dos tipos de declaraciones, declaraciones de C y declaraciones de lex, las
de C son aquellas encerradas entre dos líneas %{ y %} respectivamente que le
indican a lex, cuando se incluye código que será copiado sin modificar al archivo
generado en C (típicamente lex.yy.c).
Docente: Ing. Mirko Manrique Ronceros
~ 66 ~
Universidad Nacional del Santa
Curso: Teoría de Compiladores
Las declaraciones de lex están formadas por un nombre o identificador y su respectiva
expresión regular, su funcionamiento es análogo a aquel del "#define" del
preprocesador de C, cada vez que aparecen es como si en ese lugar estuviera escrita
la expresión regular equivalente, también se pueden usar estas para formar nuevas
expresiones regulares.
Docente: Ing. Mirko Manrique Ronceros
~ 67 ~
Descargar