Está en la página 1de 15

Tecnológico de Estudios Superiores de Ecatepec

División de Ingeniería en Sistemas Computacionales

LENGUAJES Y AUTÓMATAS I
Profesor: Stein Carrillo Juan Manuel

TAREA 1: ANÁLISIS LÉXICO


Bello Pérez Ximena
Grupo: 5503
21 de junio de 2021.
CONTENIDO
INTRODUCCIÓN............................................................................................................................3
DESARROLLO................................................................................................................................4
CAPÍTULO 3................................................................................................................................4
3.1 La función del analizador léxico.....................................................................................4
3.2 Uso de búfer en la entrada...............................................................................................6
3.3 Especificación de los tokens...........................................................................................7
3.4 Reconocimiento de tokens.............................................................................................10
CONCLUSIONES..........................................................................................................................14
BIBLIOGRAFÍA............................................................................................................................14
INTRODUCCIÓN
El Análisis Léxico es la primera fase de un compilador y consiste en un programa
que recibe como entrada el código fuente de otro programa (secuencia de
caracteres) y produce una salida compuesta de tokens (componentes léxicos) o
símbolos. Su principal función consiste en leer los caracteres de entrada y
elaborar como salida una secuencia de componentes léxicos que utiliza el
analizador sintáctico para hacer el análisis
DESARROLLO
CAPÍTULO 3
ANÁLISIS LÉXICO
Para hacer uso de un analizador léxico hecho a mano, primero es importante iniciar con
un diagrama o algún otra descripción de los lexemas de cada token, así podemos escribir
código para identificar cada ocurrencia de cada lexema en la entrada y así devolver
información acerca del token identificado. De igual manera podemos hacer uso de un
analizador léxico pero en forma automática, especificando los patrones de lexemas al
generador de analizadores léxicos, y compilando los patrones en forma de código para
que haga su trabajo como un analizador léxico, usando es método facilitamos el momento
de modificar el analizador léxico, ya que sólo reescribimos los patrones afectados en lugar
de todo el programa completo.
3.1 La función del analizador léxico
La primer fase de un compilador consiste en que la principal tarea del analizador léxico
leerá los caracteres de la entrada del programa fuente posteriormente los agrupará en
lexemas y finalmente producirá una salida qué será una secuencia de tokens para cada
lexema en el programa fuente.
El flujo de tokens es enviado al analizador sintáctico para su análisis importante decir que
frecuentemente el analizador léxico interactúa también con una tabla de símbolos.
Cuando el analizador léxico descubre un lexema que constituye a un identificador debe
introducir ese lexema dentro de la tabla de símbolos en algunos. En ocasiones el
analizador léxico puede leer la información relacionada con el tipo de información de la
tabla de símbolos como parte de ayudar a determinar el token apropiado que pasará al
analizador sintáctico.

Sabemos que el analizador léxico es parte del compilador que lee el texto aparte de
identificar lexemas, también debe realizar otras tareas, otra de esas tareas es eliminar
comentarios. Y los espacios en blancos como puede ser caracteres de espacio tabulador
y tal vez otros caracteres que se utilizan para separar los tokens. Otra de las tareas y es
correlacionar los mensajes de error que son generados por el compilador con el programa
fuente.
3.1.1 Comparación entre análisis léxico y análisis sintáctico
Hay muchas razones por las que cuales análisis de un compilador se separa en fases de
análisis léxico y análisis sintáctico (parsing).
 La sencillez en el diseño es más importante: La separación del análisis léxico y
el análisis sintáctico a menudo nos permite simplificar por lo menos una de estas
tareas.
 Mejorar la eficiencia del compilador: Un analizador léxico separado nos permite
aplicar técnicas especializadas que sirven sólo para la tarea léxica, no para el
trabajo del análisis sintáctico.
 Mejorar la portabilidad del compilador: Las peculiaridades específicas de los
dispositivos de entrada pueden restringirse al analizador léxico.
3.1.2 Tokens, patrones y lexemas

Al referirnos al análisis léxico, se usan tres términos distintos:


 Un token es un par que consiste en un nombre de token y un valor de atributo
opcional. Con frecuencia nos referiremos a un token por su nombre. 
 Un patrón es una descripción de la forma que pueden tomar los lexemas de un
token. En el caso de una palabra clave como token, el patrón es sólo la secuencia
de caracteres que forman la palabra clave.
 Un lexema es una secuencia de caracteres en el programa fuente, que coinciden
con el patrón para un token y que el analizador léxico identifica como una instancia
de ese token.

3.1.3 Atributos para los tokens


Al hablar de que más de un lexema puede coincidir con un patrón, el analizador léxico
debe proporcionar a las subsiguientes fases del compilador información adicional sobre el
lexema específico que coincidió. Por ende, en muchos casos el analizador léxico devuelve
al analizador sintáctico no sólo el nombre de un token, sino un valor de atributo que
describe al lexema que representa ese token; el nombre del token influye en las
decisiones del análisis sintáctico, mientras que el valor del atributo influye en la traducción
de los tokens después del análisis sintáctico. El ejemplo más importante es el token id, en
donde debemos asociar con el token una gran cantidad de información. Por lo general, la
información sobre un identificador se mantiene en la tabla de símbolos.

3.1.4 Errores léxicos


Es necesario contar con todos los componentes, ya que de lo contrario para un analizador
léxico le será bastante difícil saber que hay un error dentro del código fuente.
Una estrategia de corrección más general es encontrar el menor número de
transformaciones necesarias para convertir el programa fuente en uno que consista sólo
de lexemas válidos, pero este método se considera demasiado costoso en la práctica
como para que valga la pena realizarlo.
Algunas posibles acciones de recuperación de errores son:
1. Eliminar un carácter del resto de la entrada.
2. Insertar un carácter faltante en el resto de la entrada.
3. Sustituir un carácter por otro.
4. Transponer dos caracteres adyacentes.

3.2 Uso de búfer en la entrada


Tras tener ejemplo de problemas al reconocer tokens, aun no podemos estar seguros de
haber visualizado el final del identificador hasta el momento en que veamos un carácter
que no sea letra ni digito y que no forme parte del lexema para id. En C, los operadores
de un solo carácter como −, = o < podrían ser también el principio de un operador de dos
caracteres, como −>, == o <=. Por lo tanto, vamos a presentar un esquema de dos
búferes que se encarga de las lecturas por adelantado extensas sin problemas. Después
consideraremos una mejora en la que se utilizan “centinelas” para ahorrar tiempo al
verificar el final de los búferes.
3.2.1 Pares de búferes
Se han creado técnicas especializadas de uso de búferes para reducir la cantidad de
sobrecarga requerida en el procesamiento de un solo carácter entrante, esto debido a que
el tiempo para procesar caracteres y el extenso número de caracteres a procesar durante
la compilación del programa fue extenso. Un esquema importante implica el uso de dos
búferes que se recargan en forma alterna, como se sugiere en la figura:

Cada búfer es del mismo tamaño N, y por lo general N es del tamaño de un bloque de
disco (es decir, 4 096 bytes). Mediante el uso de un comando de lectura del sistema
podemos leer N caracteres y colocarlos en un búfer, en vez de utilizar una llamada al
sistema por cada carácter. Si quedan menos de N caracteres en el archivo de entrada,
entonces un carácter especial, representado por eof, marca el final del archivo fuente y es
distinto a cualquiera de los posibles caracteres del programa fuente.
Se mantienen dos apuntadores a la entrada:
1. El apuntador inicioLexema marca el inicio del lexema actual, cuya extensión
estamos tratando de determinar.
2. El apuntador avance explora por adelantado hasta encontrar una coincidencia en
el patrón
Una vez determinado el siguiente lexema, avance se colocará en el carácter del extremo
derecho, luego una vez registrado el lexema como un valor de atributo de un token
devuelto del analizador sintáctico, iniciolexema será colocado en el carácter después
del lexema que acabamos de encontrar.
3.2.2 Centinelas
Basándonos en el ejemplo anterior debemos de verificar que cada que movamos el
apuntador avance, no hayamos salido de 1 de los búferes, si esto llegara a pasar
entonces debemos de recargar al siguiente. por lo tanto cada lectura de caracteres
deberá tener dos pruebas: una para el final del buffer y la otra para determinar el carácter
que se leerá. Ahora bien podemos hacer una combinación de ambas si tenemos cada
buffer para que contenga un valor centinela al final Se dice que el centinela es aquel
carácter especial que no forma parte del programa fuente para esto una opción natural del
carácter es eof.
Observe que eof retiene su uso como marcador del final de toda la entrada. Cualquier eof
que aparezca en otra ubicación distinta al final de un búfer significa que llegamos al final
de la entrada.

3.3 Especificación de los tokens


Para poder especificar patrones de lexemas las expresiones regulares son la anotación
de suma importancia que siempre debemos de considerar; a pesar de no poder expresar
todos los patrones posibles son muy efectivas para poder especificar el tipo de patrón qué
necesitamos para cada token.
¿Se nos puede acabar el espacio de los buffers?
dentro de los lenguajes modernos, los lexemas son cortos y basta con 1 o dos caracteres
de lectura adelantada, por ende un tamaño de buffer N alrededor de los miles es más que
suficiente y el esquema de doble buffer funciona sin problemas. Pero aún existen cierto
riesgos. Por ejemplo las cadenas de caracteres pueden ser muy extensas esto provocaría
tener varias líneas y enfrentaríamos la responsabilidad de un lexema más grande que N;
para evitar esto podemos tratarlas como una concatenación de componentes 1 de cada
línea sobre la cual se escribe la cadena. En Java es convencional representar cadenas
extensas escribiendo una parte en cada línea y concatenando las partes con un operador
al final de cada parte.
Otro problema que enfrentamos ocurre cuando puede ser necesario un proceso de lectura
adelantada arbitrariamente extenso. Como es el caso PL/I no tratan a las palabras clave
como reservadas quiere decir podemos usar identificadores con el mismo nombre que
una palabra clave; por esta razón los lenguajes modernos tienden a reservar sus palabras
claves.
3.3.1 Cadenas y lenguajes
Podemos decir que un alfabeto será el conjunto finito de símbolos; un claro ejemplo típico
de símbolos son letras dígitos y los signos de puntuación. El conjunto {0, 1} es el alfabeto
binario. ASCII es un ejemplo importante de un alfabeto; se utiliza en muchos sistemas de
software. Unicode, que incluye aproximadamente 10 000 caracteres de los alfabetos
alrededor del mundo, es otro ejemplo importante de un alfabeto.
Al hablar sobre una cadena dentro de un alfabeto nos referimos a una secuencia finita de
símbolos que se extraen de ese mismo alfabeto; como parte del lenguaje decimos que
una oración o palabra a menudo es utilizado como símbolo de cadena.
Un lenguaje es cualquier conjunto contable de cadenas dentro de algún alfabeto fijo. Los
lenguajes abstractos como ∅, el conjunto vacío, o {∈}, el conjunto que contiene sólo la
cadena vacía, son lenguajes bajo esta definición.
Términos para partes de cadenas
Los siguientes términos relacionados con cadenas son de uso común:
1. Un prefijo de la cadena s es cualquier cadena que se obtiene al eliminar cero o
más símbolos del final de s. Por ejemplo, ban, banana y ∈ son prefijos de banana.
2. Un sufijo de la cadena s es cualquier cadena que se obtiene al eliminar cero o más
símbolos del principio de s. Por ejemplo, nana, banana y ∈ son sufijos de banana.
3. Una subcadena de s se obtiene al eliminar cualquier prefijo y cualquier sufijo de s.
Por ejemplo, banana, nan y ∈ son subcadenas de banana.
4. Los prefijos, sufijos y subcadenas propios de una cadena s son esos prefijos,
sufijos y subcadenas, respectivamente, de s que no son ∈ ni son iguales a la
misma s.
5. Una subsecuencia de s es cualquier cadena que se forma mediante la eliminación
de cero o más posiciones no necesariamente consecutivas de s. Por ejemplo,
baan es una subsecuencia de banana.
3.3.2 Operaciones en los lenguajes
Las operaciones de mayor importancia en los lenguajes son la Unión la concatenación y
la cerradura. La Unión será la operación familiar que hace con los conjuntos. la
concatenación del lenguaje es cuando se unen todas las cadenas que se forman al tomar
una cadena del primer lenguaje y una cadena del segundo lenguaje en todas las formas
posibles. La cerradura (Kleene) de un lenguaje L, que se denota como L*, es el conjunto
de cadenas que se obtienen al concatenar L cero o más veces. Observe que L0, la
“concatenación de L cero veces”, se define como { ∈}, y por inducción, Li es Li−1L. Por
último, la cerradura positiva, denotada como L+, es igual que la cerradura de Kleene, pero
sin el término L0. Es decir, ∈ no estará en L+ a menos que esté en el mismo L.
3.3.3 Expresiones regulares
En un ejemplo anterior describimos los identificadores dándoles nombres a los conjuntos
de letras y dígitos, usando operadores de Unión como concatenación y cerradura del
lenguaje. Este proceso es tan útil utilizar comúnmente una canción conocida como
expresiones regulares, con la finalidad de describir a todos los lenguajes que pueden
construirse a partir de estos operadores, aplicados a los símbolos de cierto alfabeto. En
esta notación, si letra_ se establece de manera que represente a cualquier letra o al guion
bajo, y dígito_ se establece de manera que represente a cualquier dígito, entonces
podríamos describir el lenguaje de los identificadores de C mediante lo siguiente:
letra_( letra_ | dígito )*

BASE: Hay dos reglas que forman la base:

1. ∈ es una expresión regular, y L(∈) es {∈}; es decir, el lenguaje cuyo único miembro es
la cadena vacía.
2. Si a es un símbolo en Σ, entonces a es una expresión regular, y L(a) = {a}, es decir, el
lenguaje con una cadena, de longitud uno, con a en su única posición. Tenga en cuenta
que por convención usamos cursiva para los símbolos, y negrita para su correspondiente
expresión regular.
INDUCCIÓN: Hay cuatro partes que constituyen la inducción, mediante la cual las
expresiones regulares más grandes se construyen a partir de las más pequeñas. Suponga
que r y s son expresiones regulares que denotan a los lenguajes L(r) y L(s),
respectivamente.
1. (r)|(s) es una expresión regular que denota el lenguaje L(r) ∪ L(s).
2. (r)(s) es una expresión regular que denota el lenguaje L(r)L(s).
3. (r)* es una expresión regular que denota a (L(r))*.
4. (r) es una expresión regular que denota a L(r). Esta última regla dice que
podemos agregar pares adicionales de paréntesis alrededor de las
expresiones, sin cambiar el lenguaje que denotan.
Según su definición, las expresiones regulares a menudo contienen pares innecesarios de
paréntesis. Tal vez sea necesario eliminar ciertos pares de paréntesis, si adoptamos las
siguientes convenciones:
a) El operador unario * tiene la precedencia más alta y es asociativo a la izquierda.
b) La concatenación tiene la segunda precedencia más alta y es asociativa a la
izquierda.
c) | tiene la precedencia más baja y es asociativo a la izquierda.
3.3.4 Definiciones regulares
Lo más conveniente es poner nombres a ciertas expresiones regulares, utilizarlos en
expresiones subsiguientes, como si los nombres fueran símbolos por sí mismos. Si Σ es
un alfabeto de símbolos básicos, entonces una definición regular es una secuencia de
definiciones de la forma:

En donde decimos:
1. Cada di es un nuevo símbolo, que no está en Σ y no es el mismo que cualquier
otro d.
2. Cada ri es una expresión regular sobre el alfabeto Σ ∪ {d1, d2, …, di−1}.
3.3.5 Extensiones de las expresiones regulares
A partir de que Kleene, introduce las expresiones regulares con los operadores básicos
para la Unión, la concatenación y la cerradura de Kleene en 1950, agregado muchas
extensiones a las expresiones regulares para mejorar su habilidad al especificar los
patrones de cadenas.
 Una o más instancias. El operador unario postfijo + representa la cerradura
positivo de una expresión regular y su lenguaje. Es decir, si r es una expresión
regular, entonces (r)+ denota el lenguaje (L(r))+. El operador + tiene la misma
precedencia y asociatividad que el operador *. Dos leyes algebraicas útiles, r* = r+|
∈ y r+ = rr * =r *r relacionan la cerradura de Kleene y la cerradura positiva.
 Cero o una instancia. El operador unario postfijo ? significa “cero o una
ocurrencia”. Es decir, r? es equivalente a r|∈, o dicho de otra forma, L(r?) = L(r) ∪ {
∈}. El operador ? tiene la misma precedencia y asociatividad que * y +.
 Clases de caracteres. Una expresión regular a1|a2|· · ·|an, en donde las ais son
cada una símbolos del alfabeto, puede sustituirse mediante la abreviación
[a1a2···an ]. Lo que es más importante, cuando a1, a2, …, an forman una
secuencia lógica, por ejemplo, letras mayúsculas, minúsculas o dígitos
consecutivos, podemos sustituirlos por a1-an; es decir, sólo la primera y última
separadas por un guión corto. Así, [abc] es la abreviación para a|b|c, y [a-z] lo es
para a|b|· · ·|z
3.4 Reconocimiento de tokens
Ahora analizaremos cómo tomar todos los patrones para todos los tokens necesarios y
construir una pieza de código para examinar la cadena de entrada y hallar un prefijo que
sea un lexema que coincida con 1 de sus patrones.

Esta sintaxis es similar a la del lenguaje Pascal porque then aparece en forma explícita
después de las condiciones. Para oprel, usamos los operadores de comparación de
lenguajes como Pascal o SQL, en donde = es “es igual a” y <> es “no es igual a”, ya que
presenta una estructura interesante de lexemas.
Las terminales de la gramática, que son if, then, else, oprel, id y numero, son los nombres
de tokens en lo que al analizador léxico respecta. Los patrones para estos tokens se
describen mediante el uso de definiciones regulares.

El analizador léxico reconocerá las palabras clave if, then y else, así como los lexemas
que coinciden con los patrones para oprel, id y numero. Para simplificar las cosas, vamos
a hacer la suposición común de que las palabras clave también son palabras reservadas;
es decir, no son identificadores, aun cuando sus lexemas coinciden con el patrón para
identificadores.
3.4.1 Diagramas de transición de estados
Los diagramas de transición de estados tienen una colección de nodos o círculos,
llamados estados. Cada estado representa una condición que podría ocurrir durante el
proceso de explorar la entrada, buscando un lexema que coincida con uno de varios
patrones. Podemos considerar un estado como un resumen de todo lo que debemos
saber acerca de los caracteres que hemos visto entre el apuntador inicioLexema y el
apuntador avance.
Las líneas se dirigen de un estado a otro del diagrama de transición de estados. Cada
línea se etiqueta mediante un símbolo o conjunto de símbolos. Si nos encontramos en
cierto estado s, y el siguiente símbolo de entrada es a, buscamos una línea que salga del
estado s y esté etiquetado por a.Si encontramos dicha línea, avanzamos el apuntador
avance y entramos al estado del diagrama de transición de estados al que nos lleva esa
línea. Asumiremos que todos nuestros diagramas de transición de estados son
deterministas, lo que significa que nunca hay más de una línea que sale de un estado
dado, con un símbolo dado de entre sus etiquetas.
 Se dice que ciertos estados son de aceptación, o finales.
 Además, si es necesario retroceder el apuntador avance una posición.
 Un estado se designa como el estado inicial.

3.4.2 Reconocimiento de las palabras reservadas y los identificadores


El reconocimiento de las palabras reservadas y los identificadores presenta un problema.
Por lo general, las palabras clave como if o then son reservadas (como en nuestro
bosquejo), por lo que no son identificadores, aun cuando lo parecen.

Hay dos formas en las que podemos manejar las palabras reservadas que parecen
identificadores:
1. Instalar las palabras reservadas en la tabla de símbolos desde el principio. Un
campo de la entrada en la tabla de símbolos indica que estas cadenas nunca
serán identificadores ordinarios, y nos dice qué token representan.
2. Crear diagramas de transición de estados separados para cada palabra clave; en
la figura 3.15 se muestra un ejemplo para la palabra clave then. Observe que
dicho diagrama de transición de estado consiste en estados que representan la
situación después de ver cada letra sucesiva de la palabra clave, seguida de una
prueba para un “no letra ni dígito”, es decir, cualquier carácter que no pueda ser la
continuación de un identificador.

3.4.3 Finalización del bosquejo


El diagrama de transición de estados para el token numero se muestra en la figura 3.16, y
es hasta ahora el diagrama más complejo que hemos visto. Empezando en el estado 12,
si vemos un dígito pasamos al estado 13. En ese estado podemos leer cualquier número
de dígitos adicionales. No obstante, si vemos algo que no sea un dígito o un punto, hemos
visto un número en forma de entero; 123 es un ejemplo. Para manejar ese caso pasamos
al estado 20, en donde devolvemos el token numero y un apuntador a una tabla de
constantes en donde se introduce el lexema encontrado. Esta mecánica no se muestra en
el diagrama, pero es análoga a la forma en la que manejamos los identificadores.

El diagrama de transición de estados final, es para el espacio en blanco. En ese diagrama


buscamos uno o más caracteres de “espacio en blanco”, representados por delim en ese
diagrama; por lo general estos caracteres son los espacios, tabuladores, caracteres de
nueva línea y tal vez otros caracteres que el diseño del lenguaje no considere como parte
de algún token.

3.4.4 Arquitectura de un analizador léxico basado en diagramas de transición de


estados
Cualquiera que sea la estrategia general, cada estado es por un fragmento de código.
Podemos imaginar una variable estado que contiene el número del estado actual para un
diagrama de transición de estados. Una instrucción switch con base en el valor de estado
nos lleva al código para cada uno de los posibles estados, en donde encontramos la
acción de ese estado. A menudo, el código para un estado es en sí una instrucción switch
o una bifurcación de varias vías que determina el siguiente estado mediante el proceso de
leer y examinar el siguiente carácter de entrada.
CONCLUSIONES
Personalmente considero que los analizadores léxicos hoy en día han permitido
conocer las principales fases en que se desarrollará un compilador, podemos decir
que nos permite conocer las partes que compondrán el programa fuente, de otra
manera los analizadores léxicos han permitido al hombre resolver problemas que
pueden llegar a surgir a causa de un programa que no tenga congruencia o que
simplemente no es fácil de entender al primer momento, o bien estructurado claro
dependerá de nosotros y de los conocimientos que vayamos adquiriendo a ver
qué tan capaces somos de resolver estos problemas. Es cuestión de poner en
práctica lo aprendido porque personalmente no me quedo claro aún de qué
manera los analizadores léxicos se pueden utilizar quizá realizar ejercicios en
clase me ayudará a entender mejor.

BIBLIOGRAFÍA
Hopcroft, J. E., Motwani, R., & Ullman, J. D. (2007). Introducción a la Teoría de
autómatas, lenguajes y computación. Madrid: PEARSON EDUCACIÓN S.A.
AHO, A. V. (2008). COMPILADORES. PRINCIPIOS, TÉCNICAS Y
HERRAMIENTAS. Segunda edición. México: PEARS ON EDUCACIÓN,.

También podría gustarte