Análisis de Compiladores en Barcelona
Análisis de Compiladores en Barcelona
Y
SINTÁCTICO DE UN COMPILADOR
Joel Ayala de la Vega Irene Aguilar Juárez José Jair Vázquez Palma
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
Indexación de Datos
Bases de datos en las que Editorial Centro de Estudios e Investigaciones para el Desarrollo Docente
CENID A.C. está indexada: Dialnet (Universidad de la Rioja)
© 2021 Editorial Centro de Estudios e Investigaciones para el Desarrollo Docente. CENID AC
Av. Acantilado 2880 Int. Mirador de la barranca 43, Col. Bosques el centinela, C.P. 45120, Zapopan, Jal.
Teléfono: 01 (33) 1061 8187 Registro definitivo Reniecyt No.1700205 a cargo de Conacyt.
CAPÍTULO I
Compiladores ………………………………………………........... 6
CAPÍTULO II
CAPÍTULO III
CAPÍTULO IV
CAPÍTULO V
CAPÍTULO VI
El desarrollo de los compiladores facilitó el uso masivo de las computadoras digitales por-
que ofreció al público general los medios amigables para incursionar en la codificación de
aplicaciones informáticas útiles en cualquier área de estudio humano. Sin los compiladores
el desarrollo de los programas informáticos hubiera quedado en el dominio de pocas per-
sonas especialistas y no se hubieran aprovechado las computadoras de la forma masiva
Aunque es muy probable que los profesionistas del cómputo no tengan que enfrentar el reto
de desarrollar o mantener un compilador durante su vida profesional, el estudio de las bases
teóricas de estos programas informáticos facilita que los alumnos comprendan con mayor
profundidad la aplicación de la teoría de autómatas y leguajes formales, la aplicación de las
estructuras de datos complejas, las técnicas de programación estructurada y el desarrollo
de software.
Este libro, en concreto, está constituido en seis capítulos en los que se explican los funda-
mentos teóricos del funcionamiento de un compilador, de ahí que haya sido diseñado para
ser utilizado como fuente de consulta en un curso semestral de compiladores. En síntesis,
en el capítulo I se describe el proceso de la compilación y sus fases, mientras que en el
capítulo II se explica la teoría de los lenguajes formales. En el capítulo III se analizan las
bases teóricas de los lenguajes regulares y sus operaciones, y en el capítulo IV se analiza la
teoría de autómatas de estado finito y su relación con el análisis lexicográfico. En el capítulo
V se describen las gramáticas formales en general y las gramáticas libres de contexto en
particular, así como su utilidad en el análisis sintáctico de un compilador. Finalmente, en el
capítulo VI se analizan los autómatas tipo pila y los diferentes tipos de analizadores. Como
estrategia didáctica se presenta el anexo A, el cual constituye un ejemplo de la codificación
de un analizador sintáctico ascendente donde se colocan los diagramas de flujo y el código
en lenguaje C indicando la forma en que se puede programar una pila de apuntadores para
la construcción del árbol en forma ascendente para que el alumno analice la aplicación de
los analizadores SLR. Por último, en el anexo B se ofrece el código completo.
6 CAPÍTULO I.
COMPILADORES
INTRODUCCIÓN
Un compilador es, en pocas palabras, un traductor que usa como entrada un leguaje de alto
nivel y da como resultado otro lenguaje aproximado al lenguaje máquina. El lenguaje de
programación C, por ejemplo, se compila comúnmente a un lenguaje ensamblador, el cual
es convertido en lenguaje máquina por un ensamblador.
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
Un compilador efectúa solo la traducción (no ejecuta el programa), mientras que el progra-
ma objeto que resulta después de la compilación será directamente ejecutable.
COMPILACIÓN
Si decidimos tomar las definiciones análisis y síntesis literalmente, tenemos que el análisis
se lleva a cabo mediante tres etapas:
1. Análisis lexicográfico.
2. Análisis sintáctico.
3. Análisis semántico.
ANÁLISIS LEXICOGRÁFICO
El objetivo del analizador lexicográfico es examinar cada una de las palabras contenidas
en un programa de un lenguaje de programación específico. El analizador lexicográfico —
también conocido como scanner— lee carácter por carácter hasta formar una palabra que
puede ser uno de los siguientes elementos:
• identificadores,
• delimitadores,
• símbolos de operadores,
• números,
• palabras clave,
• palabras pregonadas (palabras opcionales que se insertan en los
enunciados para mejorar la legibilidad),
• espacios en blanco,
• comentarios, etc.
Todos con alguna relación entre sí (tokens). Cada token es una secuencia de caracteres
8 tratados como una entidad, y son la entrada para la siguiente etapa del compilador.
El modelo básico que se usa para proyectar analizadores léxicos es el autómata de estados
finitos. Se utilizará al lenguaje C estándar para indicar un error lexicográfico. Si se escribe
la función principal como:
mein
main
Si la palabra está bien escrita, el compilador reconoce la palabra y la envía como token al
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
analizador sintáctico.
ANÁLISIS SINTÁCTICO
Solo como ejemplo se utilizará el español para mostrar la forma en que funciona el anali-
zador sintáctico. En español, un error sintáctico sería:
Obsérvese que cada palabra está bien escrita, por lo que en esta oración se tienen seis
tokens:
Véase que cada palabra está bien escrita (análisis lexicográfico), pero no tiene una estruc-
tura sintáctica definida (en este ejemplo la palabra a analizar por el analizador sintáctico
sería “cjlpln”, el analizador léxico envía un token “c” al analizador sintáctico y es aceptado,
el siguiente token que llega del analizador lexicográfico es una “j”, en este momento el ana-
lizador sintáctico no lo reconoce y produce un error sintáctico).
Las oraciones correctas serían las siguientes: “la niña juega con la pelota”, “la pelota juega
con la niña”, “con la pelota juega la niña” o “con la niña juega la pelota”. Estas cuatro oracio-
nes están bien definidas sintácticamente, por lo que las posibles palabras para el análisis
sintáctico serían “lnjclp” o “lpjcln” o “clpjln” o “clnjlp”.
Para el analizador sintáctico un programa escrito en un lenguaje de programación imperati-
9
vo, sin importar lo grande que sea, es una palabra para reconocer, donde cada token que le
llega representa un símbolo de tal palabra.
El analizador sintáctico (también conocido como parser) recibe como entrada los tokens que
le pasa el analizador léxico, comprueba que lleguen en el orden correcto (definido por el
lenguaje) e identifica las estructuras de programa (enunciados, declaraciones expresiones,
etc.). El analizador sintáctico se alterna con el análisis lexicográfico hasta que llega el fin del
programa, por lo que se crea un ciclo de la siguiente forma:
• Primero. El analizador lexicográfico lee una palabra del programa que debe
Se utilizará el lenguaje C estándar para indicar un error sintáctico si se escribe una repeti-
ción, utilizando el comando “for” de la siguiente forma:
Es posible que no se haya roto ninguna regla lexicográfica, pero sí una sintáctica, ya que
los elementos dentro del paréntesis no tienen el orden adecuado (como primer elemento
el inicio del contador; como segundo elemento la condición de continuidad, y como tercer
elemento el incremento o decremento del contador). La sentencia correcta sería:
ANÁLISIS SEMÁNTICO
Obsérvese que cada palabra está bien escrita (análisis lexicográfico) y bien colocada (sin-
10 tácticamente hablando), pero no tiene un sentido apropiado (al menos que se esté en el
mundo de los Tiny Tunes), por lo que presenta un problema semántico.
Se mostrará un ejemplo por medio del lenguaje de programación C para comprender mejor
el análisis semántico:
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
#include <stdio.h>
main(){
int i, *ptr;
i=10;
ptr=i;
}
El programa está bien escrito tanto en forma lexicográfica como sintáctica. Al recorrer el ár-
bol sintáctico, se puede observar que en la línea 5 se tiene un error de tipo, ya que se indica
una conversión inválida de tipos de “entero a entero”. En tal línea (“ptr=i;”) se está tratando
de dar a la variable “ptr” un valor entero cuando espera una variable tipo apuntador de ente-
ro. Esto se puede observar solo hasta que el árbol sintáctico esté creado.
El programa correcto es:
11
#include <stdio.h>
main(){
int i, *ptr;
i=10;
ptr=&i;
}
OPTIMIZACIÓN DE CÓDIGO
ENLAZADOR
Permite formar un solo programa a partir de varios archivos de código máquina relocaliza-
ble. Estos archivos pueden haber sido el resultado de varias compilaciones distintas, y al-
gunos de estos pueden ser librerías del sistema que pueden utilizarse en programas fuente
(ejemplo de esto son todos los archivos que se localizan en el subdirectorio “include” de
cualquier compilador en C, como “stdio.h”, “stdlib.h”, etc.).
12 GENERADOR DE CÓDIGO
Este proceso implica dar formato apropiado a la salida con base en la información que
contiene la representación interna del programa (Cueva Lovelle, 2001; Dean, 2001; Proces-
sadors de llenguatge II, 25 de marzo de 2020). El código de salida puede ser directamente
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
ejecutable o puede haber otros pasos de traducción por seguir: por ejemplo, ensamblador o
vinculación y carga (en el caso de Java y otros lenguajes compilados-interpretados, se crea
un código de bytes que será interpretado por una máquina virtual).
INTRODUCCIÓN
Existen dos tipos básicos y reconocidos de lenguajes: los lenguajes naturales o semánticos
y los lenguajes formales. El origen y desarrollo de los primeros —como el castellano, el
inglés o el francés— es natural, es decir, sin el control de ninguna teoría. Las teorías de len-
El conflicto que se tiene con un lenguaje semántico es su ambigüedad, ya que una palabra,
frase u oración puede tener en un momento dado más de un significado, dependiendo del
contexto en el que se esté hablando (p. ej., La planta de los pies y La planta de un edificio).
Otra ventaja de los lenguajes formales es el manejo de los espacios abstractos. Esta capaci-
dad permite la exploración de nuevos espacios, lo cual es realizado por los matemáticos. La
creación de nuevos espacios permite elaborar herramientas para que la ciencia comunique
sus hallazgos sin ambigüedades.
Los lenguajes formales se pueden dividir en dos grupos: los que permiten expresar un
espacio continuo (donde el cálculo infinitesimal es el rey), y los que permiten expresar un
espacio discreto (p. ej., la teoría de grafos, la lógica, la teoría de distribuciones de probabi-
lidad discretas, la teoría de conjuntos, etc.). Dentro de los lenguajes que se localizan en el
espacio discreto están aquellos que estudian la forma en que se concatenan las palabras
que cumplen una cierta propiedad. La gran ventaja de los lenguajes formales es que permi-
ten expresar un algoritmo sin el problema de la ambigüedad. En este punto nace una nueva
disciplina matemática conocida como teoría de la computación.
Este fue el punto de partida de los lenguajes de programación, los cuales sirvieron para
expresar un algoritmo por medio de un lenguaje formal y ejecutarlo para ver y analizar su
comportamiento.
trucciones de manera más rápida y eficiente (Moral Callejón, s. f.). Posteriormente, surgió la
introducción de lenguajes de programación de alto nivel, lo que propició reglas sintácticas
y semánticas más rígidas, concretas y bien definidas que fueron útiles para apegarse aún
más al lenguaje natural, aunque con un control más riguroso, lo que dio paso a la construc-
ción de gramáticas y reglas del lenguaje.
Así, se tiene a la teoría de la computación como una herramienta matemática que permite
dar un rigor más amplio al diseño de lenguajes de programación, además de desarrollar las
bases necesarias para la construcción de gramáticas y autómatas, aunque vale señalar que
su origen se encontró afuera del vasto campo de las ciencias de la computación, es decir, la
lingüística (Cueva Lovelle, 2001).
En 1956, Chomsky creó una jerarquía en la cual se observan cuatro tipos de lenguajes (en
este escrito se analizarán los lenguajes del tipo 3 y 2. Si se desea conocer los lenguajes del
tipo 1 y 0, se recomienda leer cualquier libro sobre teoría de la computación; ver figura 2.1),
lo que posibilitó que surgiera uno de los primeros lenguajes de programación de alto nivel
implementado por gramáticas libres de contexto: ALGOL 60 (Cueva Lovelle, 2001), donde
se observa un diseño más riguroso de algoritmos de traducción y compiladores (Quiroga
Rojas, 2008).
Figura 2.1. Jerarquía de Chomsky (Google imágenes)
15
Alfabeto (). Al igual que todo lenguaje, la base de un lenguaje formal es un alfabeto, el cual
se define como un conjunto finito y no vacío de símbolos, denotado por el símbolo Σ. A con-
tinuación, se muestran algunos ejemplos de alfabetos:
Cadena. Una cadena o palabra (ω) es una serie arbitraria de símbolos que pertenecen a un
alfabeto, unidos por concatenación; por ejemplo, aaabbbccc es una cadena. Al no tener un
16 tamaño u orden específico, puede definirse simplemente que ω es una cadena o palabra.
De igual forma, existe la cadena vacía, representada con el símbolo ξ (ξ no es el espacio
en blanco. Se puede comentar que una palabra “ab” puede represe también como “ξaξbξ”,
ambas cadenas son equivalentes).
Longitud de una cadena. La longitud de una cadena w sobre , |w|, es el número de sím-
bolos que tiene la cadena. Todo símbolo del alfabeto es una palabra o cadena sobre dicho
alfabeto. La cadena vacía ( ) es una palabra sobre cualquier alfabeto.
Concatenación. Sean “x” y “y” cadenas sobre , la concatenación de “x” con “y” es la ca-
dena “z” que se obtiene al añadir la cadena “x” a la palabra “y”.
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
• Notación: z=xy,
Lenguaje (L). Es un conjunto finito o infinito de palabras o cadenas que cumplen con una
propiedad. Los siguientes son ejemplos de lenguajes:
Con base en las definiciones anteriores, se puede indicar que existe un espacio o universo
17
U infinito de lenguajes. No todos los lenguajes que se encuentran en este universo son de
interés para la teoría de los lenguajes formales, cuyo objetivo es investigar si existe un orden
dentro de ese universo para estudiar las diversas propiedades con las cuales se pueden
calificar como interesantes (Baliri, 2014).
La teoría de los lenguajes formales estudia los lenguajes prestando atención úni-
camente a sus propiedades estructurales para definir clases de complejidad es-
tructural y establecer relaciones entre las diferentes clases.
El estudio de los lenguajes formales es bastante amplio y complejo. Dentro de esta clasi-
ficación, el lenguaje regular obtiene su nombre por la regularidad o por la repetición de un
mismo componente, es decir, se tiene una precepción intuitiva del lenguaje (Quiroga Rojas,
2008).
Los lenguajes regulares contienen una estructura más simple dentro de la jerarquía de
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
Chomsky; una de las características más relevantes de los lenguajes regulares es la depen-
dencia lineal que poseen sus cadenas, es decir, la presencia de un símbolo u otro en una
posición determinada de la cadena depende exclusivamente del símbolo que lo precede
inmediatamente.
Un lenguaje L es regular si, y solo si, se cumple al menos una de las siguientes condiciones:
• L es finito.
L = R1 U R2 o L = R1R2, respectivamente.
Como se mencionó en la sección anterior, un lenguaje regular puede ser representado por la
notación de conjuntos, pero es mejor el manejo de una notación en la que al representar un
lenguaje se haga con simple texto (cadenas de caracteres). De esta manera, el representar
Hay dos tipos de expresiones regulares: las expresiones regulares atómicas y las expresio-
nes regulares compuestas. Cada tipo posee tres subtipos.
UNIÓN
CONCATENACIÓN
L1 L2 o L1L2={001,10,111,00101,1001,11101}.
Como podemos observar, las tres primeras cadenas son pertenecientes al lenguaje L1 y las
consiguientes son la concatenación de L1 con L2. La razón de las tres primeras cadenas se
debe a una de las propiedades de las expresiones regulares, la cual mantiene toda cadena
sin alteración alguna al ser concatenada a la cadena vacía (Navarrete Sánchez et al., 2008).
Desglosándolo:
La clausura de Kleene toma su nombre de quien ideó la notación para las expresio-
nes regulares, Stephen Cole Kleene (1909-1994), quien de igual forma dio pauta a la
existencia de este operador (Hopcroft, Motwani y Ullman, 2007).
Por ello, cualquier subconjunto de ∑* será llamado lenguaje. Así, ∑, Φ, ∑* son lenguajes.
En este punto, definiendo la clausura de Kleene se puede dar una definición de lenguaje
23
formal:
Para los propósitos computacionales, P es una expresión matemática bien formada o una
descripción adecuada de las cadenas en un lenguaje donde se puede dar un procedimiento
sistematizado para decidir si dada una cadena w tiene la propiedad P. De esta forma, uno
debe concretarse a definir un “procedimiento sistematizado” o “algoritmo”.
Un algoritmo puede ser descrito como una finita secuencia de instrucciones en forma pre-
cisa que, cuando se confronta con una pregunta de algún tipo y ejecutado con los pasos
Un ejemplo de una propiedad que no cumple con algún procedimiento sistematizado es:
El ejemplo describe en forma adecuada la cadena del lenguaje, pero no tiene en general un
procedimiento o algoritmo en el que pueda indicar si la cadena w pertenece o no al lenguaje.
Obsérvese que no existe restricción en el tamaño de la cadena.
L={ξ,0,00,11,000,011,101,110,0000,0011,0101,1001,1010,1100,…}
Este lenguaje también se puede expresar en forma implícita. Siendo este lenguaje regular,
su expresión regular es:
L=0*(10*10*)*
Para comprender mejor la expresión regular se mostrarán las primeras cuatro cadenas indi-
cadas en el orden lexicográfico:
{ξ} => 00( )0
{0}=> 01( )⁰
{00} => 02( )⁰
{11} => 00 ( )1 = ξ(0⁰10⁰10⁰)
24 La generación de una cadena más grande sería:
Ejemplo 3.1. L= {w {0,1}* │w tiene dos o tres ocurrencias de unos, la primera y la segunda
no son consecutivas}
L= {101,0101,1010,1011,…}
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
L= 0*10*010*(10* ξ)
Por ello, una expresión que puede describir a un lenguaje solo con los operadores { ,
*}, combinados con un alfabeto , se considera expresión regular.
L={a,aa,ba,aaa,aba,baa,bba,…}
L=(a b)*a
L={ξ,a,b,c,ab,bc,cb,abc,bca,bcc,…}
L=c*(a bc*)*
Ejemplo 3.6. L= {w {0,1} *│w forma el conjunto de cadenas que comienzan con un 0 segui-
do de cualquier cantidad de números 1}.
L=0 1*
Ejemplo 3.7. L= {w {a, b} *│w forma las cadenas de letras a y b que contienen una cantidad
impar de letras b}.
L= {b, ab, ba, aab, aba, baa, bbb, aaab,aaba, abaa, ba³, bbba,bbab,babb,abbb,…}
En el primer ejemplo se colocó el leguaje cuya propiedad era par de unos. Tomando eso en
cuenta, se puede definir a impar o non como par más uno, a la a la expresión regular anterior
solo se incluye al final una b cumpliendo con dicha propiedad:
L=a*(ba*ba*)*ba*
Ejemplo 3.8. L= {w {a, b} *│w forma las cadenas de letras a y b que nunca contienen tres
letras b seguidas}.
(a U ba U bba)* (ξ U b U bb)
Como ejemplo, un número indicado en forma general en el orden lexicográfico puede ser:
Por tanto, un lenguaje es regular si puede ser descrito por medio de una expresión regular.
L2=a*b*
L2={ ξ,a,b,aa,ab,bb,aaa,aab,abb,bbb,…}
Véase que el operador Kleene no permite realizar una contabilidad de letras a con respecto
Las expresiones regulares toman varias de las propiedades que la notación de conjuntos
posee, ya que están basadas en el álgebra de conjuntos, pero con sus debidas variedades.
Tomamos que α y β son expresiones regulares equivalentes, por lo tanto, α = β y L (α) =
L (β). A continuación, se enumeran algunas de las propiedades más recurrentes de las
expresiones regulares (Navarrete Sánchez et al., 2008). Recordando que Φ es el conjunto
vacío y ξ es la cadena vacía.
28 CAPÍTULO IV.
INTRODUCCIÓN
En los capítulos anteriores se definió el lenguaje formal desde un punto de vista informático.
Para estudiar en profundidad tales lenguajes se requieren dos herramientas que apoyen
dicha labor:
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
La palabra autómata hace alusión a imitar las funciones propias de un ser vivo, de una ma-
nera más elocuente y enfocada, al movimiento. Dentro del campo de procesadores, com-
piladores e intérpretes, el movimiento no es el campo fundamental que se estudia y ma-
neja, ya que se evoca más a la simulación de procesos para transformar información. Un
autómata dentro del campo de los compiladores, procesadores, intérpretes (por mencionar
algunos) recibe información que es codificada en cadenas de símbolos, y el autómata (que
es un dispositivo que manipula las cadenas de símbolos) produce otro tipo de cadenas
de símbolos para la salida de datos (Isasi Viñuelas, Martínez Fernández y Borrajo Millán,
1997). El resultado obtenido no solo depende del último símbolo, sino de toda la secuencia
de caracteres o cadena de símbolos secuencial que se introdujo; esta definición se atribuye
a un tipo de autómata llamado máquinas secuenciales.
Para aclarar más el tema de autómatas se tiene el siguiente ejemplo, donde se usa un len-
guaje regular, se muestra su expresión regular y posteriormente se implementa un autómata
con su grafo correspondiente, forma más común para representarlo dentro del campo de las
ciencias de la computación.
Ejemplo 4.1. Obtener un autómata que acepta el lenguaje L= {w {0,1} *| w tiene par de
ceros}.
L={ξ,1,11,00,111,100,010,001,0⁴,0²1²,0101,0110, 1001,1010,1²0²,1⁴,…}
Obsérvese que se acepta la cadena vacía por la inexistencia de ceros (0/2=0 con residuo
cero, el exponente cero es par, por lo que 00= )
L=1*(01*01*)*
Utilizando los elementos que se muestran en la figura 4.1, se forma el autómata. La opera-
ción Kleene permite añadir o no la cadena o caracteres a los que afecta. En la figura 4.2, en
la transición de q0 a q0 se realiza con la lectura del símbolo “1”, representando el operador
Kleene (la transición marca que se pueden leer los números 1 que se deseen desde la ca-
dena vacía); lo mismo sucede con la transición de q1 a q1 con la lectura del símbolo “1”.
La transición q0 a q1 se realiza leyendo un cero de la cadena (en este caso se ha leído un
impar de ceros). La transición de q1 a q0 se realiza leyendo un cero (en este caso se han
leído un par de símbolos “0”); q0 es tanto estado inicial como estado final, se justifica que
sea estado final, ya que la cadena vacía también pertenece al lenguaje.
31
Figura 4.2. Diagrama de autómata
Un autómata finito determinístico es un modelo matemático con un sistema que recibe pará-
metros de entrada y posee salidas discretas, como se mencionó antes, poseyendo de igual
forma un conjunto de estados o configuraciones internas. Los autómatas finitos son capaces
de reconocer solamente un determinado tipo de lenguajes, llamados lenguajes regulares,
que pueden ser caracterizados también mediante un tipo de gramáticas denominadas gra-
máticas regulares o por medio de expresiones regulares (Moral Callejón, s. f.).
Los autómatas finitos determinísticos (AFD) se utilizan generalmente para analizar cadenas
de caracteres y verificar si pertenecen o no a un determinado lenguaje regular; asimismo,
son empleados como analizadores en la traducción de algoritmos para los ordenadores.
Este tipo de analizadores son mayormente conocidos como analizadores lexicográficos.
M=(Q,∑,s,F,δ):
• Un alfabeto de entrada ∑
• Una colección finita de estados Q.
• Un estado inicial s.
• Una colección de estados finales o de aceptación F.
• Una función, subconjunto del producto cruz , que determina el úni-
co estado siguiente para el par (q_i,σ) correspondiente al estado actual y la entrada.
A={ξ,c,ab,cc,abc,cab,ccc,abab,ccab,cabc,abcc,…}
{abcccababab}=>c⁰()⁴=(abc³)(abc⁰)(abc⁰)(abc⁰)
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
Para dar solución al Ejemplo 4.2 no solamente se deben verificar los caracteres pertene-
cientes a ω, sino que también se deben vislumbrar sus posiciones. Por ejemplo, la cadena
abc5 se encuentra en A, pero cabac3 no lo está. Para ello, se construye un diagrama que
permita determinar los distintos miembros del lenguaje.
De esta forma, y siguiendo con el Ejemplo 4.2, se obtiene el autómata mostrado en la Figura
4.3.
La tabla de donde se muestra la función de transiciones es una donde las filas son el con-
junto de estados y las columnas las entradas con las que cuenta el autómata. De esta forma,
un autómata representado mediante el diagrama de Moore puede ser presentado como una
tabla de transiciones y viceversa.
Para el Ejemplo 4.2 se obtiene una tabla de transiciones, pero primeramente es necesario
identificar el alfabeto que acepta el autómata. Como puede observarse, es posible obtener
todos los elementos de la quíntupla mencionada con anterioridad para definir el autómata y
la tabla de transiciones. El autómata acepta un alfabeto de entrada , los estados
son Q={q0,q1,q2,q3}, el estado inicial s={q0} y los estados finales F={q0,q2}. Posterior a ello, es
posible conseguir la tabla de estados (tabla 4.2).
Tabla 4.2. Tabla de estados resultante perteneciente a la figura 4.3
33
Como se observa, el conjunto de transiciones es el subconjunto del producto cruz del nú-
L={w=(a,b)*| w tiene las letras a que se deseen, pero al menos una a al final de la cadena.
Las subcadenas de letras b, si las hay, serán continuas e impares}
L={a,aa,ba,aaa,baa,aba,a⁴,ba³,abaa,aaba,aaaba,bbba,…}
a+={a,aa,aaa,a⁴,a⁵,...}
Retomando la figura 4.4, se tiene a modo de ejemplo la cadena aaaba. De esta forma, y con
lo anteriormente mencionado, es retomar el símbolo más a la izquierda para poder inicializar
el autómata; entonces se inicia con el carácter <<a>> (figura 4.5).
Una vez que el autómata ha iniciado, comienza su ejecución tras el siguiente ciclo:
Figura 4.6. Con base en el algoritmo mostrado, va cambiando el apuntador hasta finalizar
la palabra ω
Como muestra la Figura 4.6, el apuntador se va desplazando hasta finalizar la palabra se-
gún las transiciones y los estados determinados dentro del autómata hasta finalizar y acep-
tar la cadena.
Una de las diferencias fundamentales entre las diversas clases de autómatas mencionadas
se halla en si dicho control o manejo de estados es determinista o no determinista. El no
determinismo es esencialmente la habilidad de cambiar de estado en una forma que es
parcialmente determinado por el estado actual y el símbolo de entrada. Esto es, se
permiten diferentes posibles caminos para realizar la transición a uno de los estados dada
la combinación del estado actual y el símbolo de entrada. Las posibles transiciones se cla-
sifican en:
36 1. Dado un símbolo de entrada, la transición permite trasladarse a uno de los posibles ca-
minos.
El siguiente ejemplo muestra mejor estos tres criterios (Lewis y Papadimitriou, 1998):
L={ξ,ab,aba,abab,ababa,abaab,…}
Para el tercer caso, el autómata no determinístico se observa en la figura 4.9. En este caso,
no se sabe si se tienen que leer dos o tres símbolos para realizar la transición del estado
q0 al estado q0. Esto es, si se tiene la cadena que pertenece al lenguaje “ababab” y se leen
primero tres caracteres, se realiza sin problema la primera transición, quedando el resto de
la cadena “bab”, en este momento sin importar si se leen dos o tres símbolos va a marcar
un error. Véase que el aceptar o rechazar una cadena correcta depende del número de ca-
racteres que se lean en cada transición.
Figura 4.9
Por ello, se puede diseñar más de un autómata no determinístico que acepte las cadenas
de un lenguaje regular.
38 El no determinismo logra definir un lenguaje de una forma más rápida y sencilla, a diferencia
de un autómata determinístico. Una de las ventajas sobre los autómatas no determinísticos
es que permiten diseñar soluciones más flexibles dentro de los problemas presentados a
los lenguajes de alto nivel, pero es muy difícil de programar. El autómata determinístico a
veces no es fácil de diseñar, pero es fácil de programar (Hopcroft, Motwani y Ullman, 2007).
Se muestra que el autómata de la Figura 4.10 acepta las cadenas generadas por tal expre-
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
En la tabla de transiciones obtenida por el ejemplo 4.4 es posible apreciar que en algunas de
las transiciones de los estados existe más de una transición con un solo símbolo; esta parte
viene a definirse como el no determinismo de esta clase de autómatas (Navarrete Sánchez
et al., 2008), ya que —como hemos de recordar— un AFD se caracteriza por tener una sola
transacción por un símbolo de entrada determinado.
A pesar de ser autómatas, en ocasiones complejos, es un concepto útil para probar teore-
mas y simplificar descripciones. Ahora bien, un lenguaje regular que es aceptado por un
AFND también es aceptado por un AFD (Campos, 1995).
Los autómatas no determinísticos con transición de cadena vacía son parte de la familia de
39
los AFND, con sus mismas propiedades y características; la diferencia recae en sus transi-
ciones entre un estado a otro, ya que estas pueden ser realizadas sin la necesidad de con-
sumir alguna entrada de un carácter como tal (Dean, 2001). Este tipo de transiciones son
representadas por la cadena vacía (ξ). Al igual que los AFD, los AFND, los AFND- ξ son una
quíntupla que difiere de los demás en la función de transición y en la habilidad que poseen
de permitir una cantidad variada de caminos a elegir con base en el estado en el que se
encuentran y el símbolo de entrada que se tenga (Lewis y Papadimitriou, 1998); los AFND-,
a diferencia de los AFND (que también permiten la transición a varios estados con un solo
carácter) permiten dicha transición con base en el carácter nulo o cadena vacía (Hopcroft,
Este tipo de transiciones vacías o nulas dan una nueva capacidad y versatilidad a los au-
tómatas. Si se tiene una transición nula, el autómata puede quedarse en el estado actual o
cambiar a algún otro estado que permita llegar a él sin la necesidad de consumir algún ca-
rácter como entrada. La Figura 4.11 muestra un autómata, por el cual dos de sus transicio-
nes son mediante el vacío y cuentan con transiciones con caracteres con un valor no nulo.
Como se mencionó con anterioridad, y como sucede con los AFND, la tabla de transiciones
es distinta a los demás. La diferencia recae en que dentro de los caracteres del lenguaje
aceptado existe el símbolo ξ, de esta forma también se toman en cuenta aquellos cambios
de estado que sean representados por el vacío. Aunque parece un cambio muy simple, lle-
va una gran funcionalidad porque permite la lectura de diversos caracteres y un camino de
inicio a fin, con espacios invisibles que no contribuyen o afectan a la cadena de entrada. El
Ejemplo 4.5 enseña el uso de los AFND-ξ de una manera práctica.
donde σ {0,1,…,9}
Figura 4.12. Autómata resultante del Ejemplo 4.5 usando la cadena vacía en las
transiciones de un estado a otro para obtener una solución congruente
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
Como se aprecia en la Figura 4.12 la primera transición mostrada (de q0 a q1) se logra con
el símbolo “+”, con el símbolo “–” o mediante el consumo de la cadena vacía (mejor dicho,
sin el consumo de algún carácter), lo cual es redituable ante ciertos casos y permite resolver
el ejercicio propuesto de manera temporal, pues al mismo tiempo presenta otro problema.
En el estado q1 se tiene que decidir si se mantiene en el estado q1 o se tiene que realizar
una transición a q3 por medio de un símbolo σ {0, 1,…,9}. El problema recae en el uso de
los tipos de autómatas ante una máquina de estados, ya que las máquinas de estados solo
aceptan AFD; por ello, y como se ha venido comentando, el no determinismo es una manera
fácil de dar solución a diversos problemas, pero también una solución óptima en diversos
casos como el presentado con anterioridad. Resumiendo:
Hay que recordar que todo autómata del tipo AFND, sin importar a cuál familia pertenezca,
puede ser transformado en un AFD.
• Transición con cadena vacía. Se crea una función de transición vacía en la que se
sabe, a partir de un estado, todos los estados a los que uno puede llegar de forma
directa con la transición de cadena vacía (sin leer algún símbolo de entrada). Para
observar mejor esto se usará el autómata de la figura 4.13:
Figura 4.13
42 En un ordenamiento lexicográfico, las cadenas que acepta el autómata son:
L={ξ,aa,ba,bb,aab,baa,bab,bba,bbb,aabb,baab,babb,bbaa,bbab,b4,…}
L {ab,aba,abb,a⁴,a³b,aaba,abaa,abab,abba,ab³,ba³,aaba…}
Por lo que:
L={w {a,b}*|w no tiene restricción con las b. Pero si existen, solo habrá dos a con-
catenadas o una a precedida de una b}
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
El análisis lexicográfico sirve para observar las cadenas que debe aceptar o rechazar
el autómata determinístico.
Obsérvese que a partir de q0 uno puede transitar al mismo estado q0, a los estados q1,
q3 y q4 sin leer un símbolo de entrada. De esta forma, la función E(qi), Empty, se forma
con el conjunto de estados en los que se puede transitar sin leer símbolo de entrada.
Por tanto, se requiere obtener la función E(qi) de cada uno de los estados involucra-
dos en el autómata no determinístico:
• Estado inicial. El estado inicial del AFD es E(q0), llamándose el nuevo estado ‘S’,
Start. Este nuevo estado determinístico contendrá, como parte de él, al conjunto de
estados del autómata no determinístico.
En este caso:
Para el caso del estado inicial, este se formó con E(q0) = {q0,q1,q3,q4}, por lo que se
analizarán la transiciones del AFND para cada uno de los estados antes indicados.
El resultado para la letra a es:
Nota: El autómata de la figura 4.13 se analizó con las palabras que acepta y no acepta. El
siguiente punto es observar si el AFD transformado cumple también con el mismo análisis
lexicográfico; si no es así, la traducción fue incorrecta.
44 Figura 4.14. AFD resultante del AFND de la figura 4.13
Para dar solución al autómata presentado fue eficiente el generar un autómata del tipo
AFND-ξ que se muestra en la figura 4.15. En este caso, se crean dos autómatas: el primero
acepta la cadena aa*, el estado inicial se conecta con tal autómata por medio de una transi-
ción cadena vacía al estado q1, y el autómata que acepta la cadena (ab)*(ba)* que se une el
estado inicial con la transición vacío a q2.
El primer paso dentro del algoritmo (de transformación de un AFND-ξ a AFD) consiste en
separar todas aquellas transiciones que contengan más de un solo carácter. Esto para po-
der obtener una propiedad de unicidad entre transición de un estado a otro. La figura 4.16
muestra el autómata bajo la unicidad de transiciones.
Cuarto paso. Consiste en obtener todos los demás estados con base en los caracteres
pertenecientes al lenguaje para conseguir las nuevas transiciones del AFD. El comienzo
va con base en el nuevo estado inicial S, donde debe obtenerse el nuevo estado según el
conjunto de estados de transición por el cual está formado y formando los nuevos estados
sucesivamente.
Como se puede notar en las funciones anteriores, se han obtenido dos nuevos conjuntos
de estados {q3,q4 } y {q5 }, que dan lugar a Q1 y Q2, respectivamente. De esta forma, se van
a ir formando los nuevos estados y transiciones; si alguno de estos conjuntos de estados
pertenecientes al autómata no determinístico vuelve a presentarse en alguna función, no se
genera un nuevo estado en el autómata determinístico, se indica que regresa al estado ya
existente.
46
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
Como se puede apreciar en este último caso, se repite nuevamente un conjunto que apare-
ció como resultado con anterioridad. Como se comentó, no se genera un nuevo estado, sino
que se retoma al ya generado.
Véase que ya no existen nuevos estados para procesar, por lo que se pasa a construir el
47
nuevo autómata. Como se mencionó, el nuevo estado inicial es la función vacía del estado
inicial del autómata no determinístico, y para los estados finales se aplica la misma con-
jetura, haciendo estado final del autómata determinístico a todo conjunto de estados que
contenga un estado final del autómata no determinístico.
Una vez cotejados todos los estados y transiciones, se obtiene el autómata mostrado en la
Figura 4.17. De esta manera, se obtiene el nuevo autómata del tipo AFD.
A su vez, el analizador realiza funciones secundarias dentro del compilador en la interfaz del
usuario, como son:
Dentro del proceso de un compilador existen tres razones importantes para separar el aná-
lisis léxico del análisis sintáctico:
• Permite tener un diseño sencillo, es decir, separa el análisis léxico del sin-
táctico, lo que por lo general permite simplificar algunas fases posteriores
a estas, ya que con la separación se da origen a un lenguaje más claro y
comprensible.
• Se mejora la eficiencia del compilador. Al tener por separadas ambas fases
permitirá también tener procesos independientes en cada una de ellas. Por
lo tanto, el tiempo que el procesador consume leyendo el programa fuente y
dividirlo en componentes léxicos se puede sustituir con manejo de técnicas
especiales como los buffers.
• La transportabilidad del compilador. Se sabe que cada alfabeto tiene pecu-
liaridades de entrada, entre muchas otras anomalías únicas; estas peculia-
ridades se pueden limitar al analizador léxico para tener una representación
única para los caracteres o cadenas que cumplan con ciertos rubros para
descartarlos o simplemente separarlos.
Para tener una mejor comprensión de lo que es un analizador lexicográfico y cómo se reali-
za, podemos ejemplificarlo de la siguiente manera.
En general, hay un conjunto de cadenas de entrada para las cuales se produce como salida
un componente léxico o token; a este proceso podemos llamar patrón. Se dice que cada
patrón debe coincidir con un componente del conjunto; entonces, un lexema es una secuen-
cia de caracteres, un elemento del léxico con significado referencial en el programa fuente,
49
es decir, aporta a la palabra una idea comprensible con la que concuerda el patrón para un
componente léxico.
Figura 4.18
En la primera línea un tabulador muestra la palabra for, tres espacios en blanco y un salto
de línea. En la línea dos se muestran la palabra if, dos tabuladores, la palabra for y un salto
de línea. En la línea tres solo se tiene un salto de línea. En la línea cuatro se tienen dos
espacios en blanco, la palabra is y la marca de fin de archivo (EOF), por lo que el analizador
reconocerá a cada una de las palabras hasta leer el EOF (EndOfFile).
…
Int A[7][6];
Edo=0;
Llena_Tabla(A);
Realiza{
c=lee_caracter_de_archivo();
x=Transforma(c);
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
Si (x== -1){
Escribe(“La letra no pertenece al alfabeto”);
Break;
}
Edo=A[Edo][x];
Si (Edo ==-1){
Escribe(“Error, la palabra no es reconocida”);
Break;
}
Si (Edo>6)
Edo=0; // Se reconoce una palabra correcta y se inicia la lectura de
//una nueva palabra
}mientras(c!=EOF);
}
…
Ejemplo 4.7: L={w {id, +,*,(,),$}* | w realiza operaciones matemáticas simples con el ope-
rador aditivo, el operador multiplicativo y el operador de agrupación}
Para este lenguaje se colocará un símbolo de fin de cadena que será ‘$’.
L={$, id$,(id)$,id+id$,id*id$,(id+id)$,…}
La gramática regular que podría producir todas las cadenas del lenguaje es:
53
L(G)= {(* id {+ U * U ξ } { id U ξ } )* {+ U * U ξ } }* $
(En este ejemplo se utilizan las llaves como operador de agrupación, ya que los paréntesis
pertenecen al propio lenguaje por analizar).
L(M):
id+((id$
Tabla 4.8
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
Ejemplo 4.8. Realizar la expresión regular y el autómata finito de los siguientes lenguajes.
En el lenguaje L1, para poder escribir la expresión regular y el autómata finito se requirieron
55
un finito número de elementos, por lo que el lenguaje es regular.
GRAMÁTICAS FORMALES
INTRODUCCIÓN
En esta sección se explican, de forma general, las gramáticas formales y, de manera parti-
cular, las gramáticas libres de contexto, sobre las cuales se basa el análisis sintáctico.
Recordando el capítulo de los lenguajes regulares, las expresiones regulares permiten una
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
L={ξ,a,b,aa,bb,ab,aaa,bbb,abb,aab,aaa,bbb,…}
donde L(G)=a*b*
L(M):
L= {w (a,b)*|anbn, n≥0}
L={ξ,ab,aabb,aaabbb,aaaabbbb,…}
Este lenguaje requiere una contabilidad de letras a y posteriormente compararla con una
contabilidad de letras b.
Las gramáticas libres de contexto tienen esta y otras virtudes que permiten realizar el análi-
sis sintáctico de un lenguaje imperativo.
DEFINICIÓN DE GRAMÁTICAS
John Backs, un diseñador de lenguajes de programación de IBM, adoptó las reglas genera-
tivas de Chomsky para describir la sintaxis del nuevo lenguaje de programación IAL, cono-
cido en la actualidad como ALGOL 58 (1959).
Peter Naur, en su reporte sobre ALGOL 60 de 1963, identificó la notación de Backus como
la forma normal de Backus (Backus Normal Form), y la simplificó para usar un conjunto de
símbolos menor; no obstante, a sugerencia de Donald Kunth, su apellido fue agregado en
Para definir gramática primero se determinará lo que es una regla en el campo de los len-
guajes formales.
α→β
Tanto α como β son cadenas de símbolos en donde pueden aparecer elementos del alfabeto
Σ (llamados constantes o símbolos terminales) o símbolos nuevos (denominados variables
o símbolos no terminales). El símbolo “” representa producción. Por lo tanto, la expresión
anterior puede leerse como “alfa produce a beta”, es decir, en la cadena donde aparezca la
subcadena que representa “alfa” se reemplaza por la subcadena que representa a “beta”.
Por ende, una gramática, básicamente, es un conjunto de reglas.
DEFINICIÓN DE GRAMÁTICA
G=(V,T,S,P)
donde
De esta forma, la gramática de los lenguajes formales está formada con símbolos del voca-
bulario terminal . Este tipo de vocabulario se define por enumeración de los símbolos termi-
nales (Cueva Lovelle, 2001).
58 El vocabulario no-terminal V es el conjunto de símbolos introducidos como elementos au-
xiliares para poder definir la gramática. No figuran en la sentencia establecida y es definido
por enumeración de los símbolos no-terminales (Cueva Lovelle, 2001).
El símbolo inicial S es un símbolo no-terminal, el cual da inicio a aplicar las reglas de la gra-
mática para obtener las distintas cadenas del lenguaje (Cueva Lovelle, 2001).
Las producciones P son el conjunto de reglas que deben aplicarse desde el símbolo inicial
para obtener las cadenas del lenguaje. El conjunto se define con base en las enumera-
ciones de las distintas producciones, en forma de reglas o mediante un metalenguaje, por
ejemplo, BNF (Backus Naur Form) o EBNF (Extended Backus Naur Form) (Cueva Lovelle,
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
2001).
En 1956, Noam Chomsky publica Three models for the description of language, un trabajo
en el que se describen las propiedades de los tipos de lenguajes formales y correspondien-
tes gramáticas en relación con su complejidad computacional.
La Figura 5.1 muestra la clasificación de lenguajes determinada por Chomsky dentro del
universo de los lenguajes.
Gramática tipo 0
ξ→β
Este tipo de gramáticas generan lenguajes recursivamente enumerables que son aceptados
en la máquina de Turing (recuérdese que el autómata puede ser del tipo determinístico o no
determinístico) (Padilla Beltrán, 2006).
Gramática tipo 1
También son conocidas como sensibles al contexto y sus reglas de producción son de la
forma:
Generalmente, son llamadas sensibles al contexto porque se puede reemplazar por siem-
pre y cuando se esté en el contexto α…β.. Por lo tanto, las producciones de tipo:
S→γ
Son aceptadas siempre y cuando no pertenezca a la parte derecha de ninguna de las reglas
de producción (recuérdese que S es el símbolo de inicio) (Navarrete Sánchez et al., 2008).
Otra forma de ver las reglas de producción es especificando que una variable A puede ser
reemplazada por γ en una derivación directa, cuando A aparezca en el contexto de α y β.
60 Por esta última característica son llamadas sensibles al contexto (Navarrete Sánchez et al.,
2008). Cabe aclarar que en este tipo de producciones en la parte izquierda siempre tienen
una longitud menor o igual que en la parte derecha, pero nunca mayor; por lo tanto, es una
gramática no contráctil. La única excepción presente es el caso ya visto:
S→γ
Gramática tipo 2
Se denominan gramáticas libres de contexto. Estas solo permiten tener un símbolo no-ter-
minal en la parte izquierda de su producción, mientras que en la parte derecha puede ser
una combinación entre terminales y no-terminales, es decir, son de la forma:
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
Las producciones obtenidas por este tipo de gramáticas son aceptadas por los autómatas
de tipo pila. Se denominan libres de contexto porque pueden cambiar A por α, independien-
temente del contexto en que aparezca (Padilla Beltrán, 2006).
Gramática tipo 3
También llamadas regulares, comienzan sus reglas de producción por un símbolo terminal,
el cual puede llegar a ser seguido por un símbolo no-terminal.
donde
donde
Se pueden generar todas y cada una de las palabras de un lenguaje regular utilizando gra-
máticas regulares. Para verificar si una palabra pertenece a un lenguaje regular se requiere
de un autómata finito.
Se pueden generar todas y cada una de las palabras de un lenguaje libre de contexto utili-
zando gramáticas libres de contexto. Para reconocer si una palabra pertenece a un lenguaje
libre de contexto se requiere de un autómata tipo pila.
Se pueden generar todas y cada una de las palabras de un lenguaje sensible al contexto
Como es posible apreciar, y según lo mencionado hasta el momento, los diferentes tipos
de autómatas (sean determinísticos o no-determinísticos) aceptan alguno de los tipos de
lenguajes manejados por Noam Chomsky, dentro de los cuales se han basado los análisis
lexicográficos y sintácticos que han permitido y han dado paso a la resolución de diversos
problemas presentes dentro del mundo de la computación, y más apegados a los compi-
ladores que permiten el interactuar con los ordenadores con base en instrucciones en un
lenguaje más comprensible para todos, y no solo según aquel que la máquina comprende.
El principal objetivo de Chomsky y su jerarquía era demostrar que los dos primeros tipos de
gramáticas son incapaces de dar cuenta, de manera simple y general, de la complejidad de
las lenguas naturales (Gallego, 2008).
GRAMÁTICAS REGULARES
Las gramáticas regulares son aquellas reconocidas por un autómata finito; el lado derecho
de la producción debe contener símbolos terminales y un símbolo no terminal (solo uno) a
la izquierda o derecha de la producción.
L={ξ,b,bb,aa,bbb,aab,aba,baa,…}
62 La expresión regular que representa el lenguaje es:
L(G)=b*(ab*ab*)*
D→bD|E
E→B|ξ
A→bA→bB→baC→baaD→baabD→baabE→baabB→baabaC→baababC→ baaba-
baD→baababaE→ baababa ξ
Obsérvese lo siguiente:
L={ ξ,a,b,aa,bb,aaa,bbb,…}
L(G)=a* Ub*
G={{S,A,B},{a,b},S,P}
P:
S→A|B|ξ
A→Aa| ξ
B→Bb| ξ
Obsérve que la producción S → A|B equivale al operador en una expresión regular.
63
Otro ejemplo es:
L=(ab U aba)*
{abababa}=> ()3=(ab)(ab)(aba)
P:
A->B->abB->ababB->ababA->ababC->abababaC->abababaA->abababa ξ
Como se observó, las gramáticas regulares son las gramáticas más laxas de la clasificación
de gramáticas dada por Chomsky.
Las gramáticas libres de contexto fueron muy importantes para los compiladores en los
años cincuenta y sesenta, y aún en la actualidad tienen una gran importancia en los ámbitos
de XML y DTD (Document Type Definition).
Una gramática libre de contexto es un conjunto formado por cuatro partes “G = {V, T, S, P}”,
donde:
2) Poder llevar una prioridad relativa con respecto a los operadores aritmé-
ticos que pertenecen un lenguaje en particular, si estos existen.
64 Para comprender mejor la idea del balance de caracteres se tiene el ejemplo 5.2.
Un análisis lexicográfico del lenguaje nos permite observar que las siguientes palabras son
válidas:
La gramática es la siguiente:
G={S,{(,)},S,P}
P:
S→ξ
S→ SS
S→(S)
S->SS->SSS->(S)SS->(S)(S)S->(S)(S)(S)->()()()()
Otra forma de representar las reglas de producción es por medio de un árbol de derivación,
el cual para la gramática dada con la palabra ()((())())() se muestra en la figura 5.2:
{{{}{} }}
Como se puede apreciar en este simple ejemplo, un programa de lenguaje imperativo, sin
importar lo grande que sea, solo forma una palabra perteneciente a un lenguaje libre de
contexto.
Recursividad
La recursividad se expresa por medio de una o más reglas no recursivas que son la base
y una o más reglas que son recursivas, las cuales permiten hacer crecer la estructura del
lenguaje aplicándose a sí mismas una y otra vez.
A→αAβ
donde α y β es una cadena con terminales y no terminales. Pueden ser la misma cadena
vacía. Si α es la cadena vacia, se conoce como recursividad hacia la izquierda. Si β es la ca-
66 dena vacía, entonces se conoce como recursividad hacia la derecha (Ruiz Catalán, 2010).
Las gramáticas libres de contexto pueden ser recursivas ambiguas o recursivas tanto iz-
quierdas como derechas. Para explicar tales gramáticas se utilizará como ejemplo el si-
guiente lenguaje:
L={id,id+id,id+id+id,…}
Se dice que una gramática es recursiva ambigua cuando existe una cadena ∑* que tenga
más de un árbol de derivación, es decir, una cadena se puede generar a su vez por más de
un árbol de derivación.
G={E,{id,+},E,{E → E+E|id}
En la figura 5.4 se muestran dos casos de derivación de la cadena id1 +id2 + id3 para ejem-
plificar el caso de derivación ambigua.
Figura 5.4. Dos derivaciones de la cadena id1 +id2 + id3 por una gramática
recursiva ambigua
R0=id0+id1
Y se introduce R0 a la pila.
R1=R0+id2
5. Se coloca R1 en pila.
R0=id1 + id2.
R1=id0 + R0.
5. El resultado R1 se guarda en tope de pila.
Por ello, una gramática libre de contexto recursiva ambigua no sirve para desarrollar un
compilador en forma eficiente.
Para restringir el número de opciones para derivar una cadena la gramática libre de contexto
tiene dos tipos de derivaciones: la derivación a la izquierda siempre reemplaza la variable
más a la izquierda por uno de los cuerpos de sus producciones, sin embargo, la derivación
a la derecha reemplaza la variable más a la derecha por uno de sus cuerpos de sus produc-
ciones
68 Un ejemplo de gramática recursiva derecha será:
ET+E
ET
Tid
En la figura 5.5 se observa el árbol de derivación para la cadena id0 + id1 + id2 + id3+id4:
La primera operación por realizar será la que tenga el paréntesis más anidado; por lo tanto,
la primera suma será id3+id4. Posteriormente, ese resultado se sumará a id2, su resultado
acumulado se sumará a id1 y al final el resultado se sumará a id0. Obsérvese que las opera-
ciones se realizarán de derecha a izquierda.
Al momento de hacer la ejecución de la expresión en postorden no se mantiene la prioridad
69
de la suma de la evaluación de izquierda a derecha.
EE+T
ET
Tid
En la figura 5.6 se observa el árbol de derivación para la cadena id0 + id1 + id2 + id3+id4.
id1id2+id3+id4+
Al transformar la expresión a inorden queda de la siguiente forma:
(id1+id2)id3+id4+
((id1+id2)+id3)id4+
((id1+id2)+id3)+id4
Realizando las operaciones con respecto al paréntesis más anidado, se tienen las siguien-
tes sumas: primero se evalúa id1+id2. A este resultado se le suma id3, y después se le suma
id4, con lo cual se obtienen las operaciones de izquierda a derecha.
De esta forma una gramática libre de contexto recursiva izquierda tiene dos ventajas con
respecto a una expresión o gramática regulares:
70 • Permite generar todas las cadenas del tipo anbn o estructura semejante,
como ya se comentó anteriormente.
• Permite el manejo de las prioridades relativas de los operadores
algebraicos.
Para una mayor comprensión de las prioridades relativas se realizará el ejemplo 5.3 con una
gramática clásica que aparece en todos los libros que versan sobre compiladores; en este
ejemplo se manejan los operadores de suma, multiplicación y agrupación.
donde P es:
E E+T
E T
T T*F
T F
F (E )
F id
Obsérvese que la prioridad relativa indica que primero se debe sumar id1 con id2. Posterior-
mente, ese resultado se debe multiplicar con id3 y al final este total se debe sumar a id0. En
un árbol sintáctico, entre mayor prioridad relativa tenga el operador más profundo se encon-
Sumar dos elementos en tope de pila y colocar el resultado en tope de pila. Ejecutando las
instrucciones, la pila se comporta de la siguiente manera:
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
Con el lenguaje y la gramática recursiva izquierda del ejemplo 5.3, se muestra la prioridad
relativa, el árbol sintáctico y su operación en postorden.
Multiplicar dos elementos en tope de pila y colocar el resultado en tope de pila. La simula-
ción de las operaciones de la pila queda de la siguiente forma:
Primero se realiza una tabla de equivalencias y se transforma la gramática para que la es-
critura del árbol sea más ligera.
75
EN →I es
C→DB
D →I ; en ; D | I : en;
B → b E’s e.
E’s → E | E; E’s
E_A→ I := EX
E¬_IN→in( I )
E_O→o ( I )
E_N→ξ
76 EX→ EX A T | T
T→TMF|F
F → I | ( EX) | CONST
I→a|b|c|d|…|z
A→ +|-
M → *|/
DIG → 0|1|2|3|…|9
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
p id es id : en ; id : en ; b in id ; in id ; c := id id + o id e.
Para el analizador sintáctico cada token es una letra de su abecedario y el programa com-
pleto es una palabra de su lenguaje.
CAPÍTULO VI. 77
AUTÓMATAS TIPO PILA
INTRODUCCIÓN
Para poder manejar estos dos elementos se requiere un autómata de estado finito con ca-
pacidad de memoria. Esta memoria puede manejarse como una pila. Por ejemplo, para el
lenguaje:
S→aSb|ξ
a *b *
El autómata no puede determinar cuántas letras b con respecto a las a han llegado; sin em-
bargo, ¿qué sucede si al autómata le colocamos una memoria de tipo pila?
En la tabla 6.1 se muestra una simulación con el autómata y una memoria tipo pila:
78 Tabla 6.1. Autómata de la figura 6.1 usando una pila como memoria de apoyo
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
Como se observa, la estrategia es la siguiente: por cada a que se lea de la cadena, se co-
loca en la pila. Por cada b que se lea de la cadena, se revisa si existe una a en la pila para
su eliminación. Al momento de leer todos los elementos de la cadena, si la pila está vacía,
indica que la cadena estuvo balanceada de letras b con respecto a las a. A este autómata
se le llamará tipo pila.
Del lenguaje se puede derivar cantidad de lenguajes que tienen una pro-
piedad semejante. Una estrategia para poder obtener el autómata tipo pila es realizar un
análisis relajado de una expresión regular (en este caso, fue a*b*) proponiéndose un AFD,
después se analiza del uso de la pila para cumplir con el balance solicitado de elementos.
Lema. Cada lenguaje libre de contexto es aceptado por un autómata tipo pila (Lewis y
Papadimitriou, 1998).
Los autómatas tipo pila son básicamente un autómata finito con control sobre los elementos
Los autómatas tipo pila o AP son autómatas finitos que pueden ser determinísticos o no
determinísticos con transición ξ. El hecho de tener una pila dentro de un autómata permite
dar la capacidad de “recordar” toda la información que pasa a través del mismo autómata.
Una de las desventajas presentes en los AP recae en la misma capacidad que tiene de
almacenar de forma infinita información dentro de la pila, ya que solo puede acceder a la
información disponible en su pila según esta sea manipulada. Al ser una pila una estructura
del tipo LIFO (last-in, first-out), solo podrá manipular el último elemento que accedió al AP.
Figura 6.3. Autómata tipo pila es un autómata finito con una estructura de datos de pila
80 En los AP se tiene el siguiente comportamiento:
Los AP, al igual que los autómatas finitos, poseen una notación gráfica, donde en cada nodo
existe una transición del tipo “ω/α/β”:
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
La figura 6.4 muestra un ejemplo en las diversas variaciones que existen en los tipos de
transición de cada autómata y cómo varían en los que son del tipo pila.
Los AP, al igual que los autómatas finitos, poseen estados finales, los cuales permiten distin-
guir que la palabra de entrada es aceptada correctamente. Para que una palabra de entrada
sea aceptada debe cumplir las siguientes condiciones:
En primer lugar, se puede señalar que con este lenguaje es casi imposible intentar construir
un autómata finito, ya que estos no cuentan con la capacidad para contar el número de
elementos que son insertados. Por ello, es necesario hacer uso de un AP. Una vez aclara-
do, véase el grafo resultante de AP, el cual se muestra en la figura 6.5. El autómata que se
visualiza no es la solución final, ya que hace falta definir las entradas y salidas de la pila.
Una vez que se tiene este esquema, es necesario tener en cuenta cuáles serán los elemen-
tos que entrarán al autómata, qué se espera que exista en la pila y cuál sería el resultado
final en la pila. Como es posible apreciar, se tienen los estados q0 y q1, donde q1 además de
ser el último estado, también es el estado de aceptación de la cadena.
Con este esquema de los AP es necesario tener en cuenta la pila en todo momento; para
ello, se debe mantener un identificador, el cual nos dé el estado actual de la pila, cuando se
encuentre vacía. Este identificador se definirá como cadena vacía. De esta forma, es posible
comenzar con la construcción del autómata.
Para este tipo de autómatas, su función va con base en la pila; por ello, y a partir del ejem-
plo, es necesario verificar y validar el estado de la pila para utilizar la siguiente transición,
ya que estas dependen del estado actual de la pila. El autómata se muestra en la figura 6.6.
82 Figura 6.6. Autómata tipo pila para el ejemplo 6.1
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
Como ejemplo, se tratará de validar la cadena “aabb”. Como el primer elemento en entrar
a la pila es una a, se tiene en cuenta que la pila en ese momento está vacía. La figura 6.7
muestra el comportamiento tras la entrada del primer elemento al AP.
Figura 6.7. Reacción del autómata tipo pila luego de introducir el primer elemento
Si se vuelve a introducir otra "a", la pila queda como se muestra en la figura 6.8.
Si recordamos, el lenguaje marca que debe ser el mismo número de entradas de a y b; por
ello, en el estado final es necesario aplicar el mismo criterio de la transición anterior. De esta
forma, se obtiene el resultado mostrado en la figura 6.10.
Para el mismo ejemplo 6.1, la función de transiciones para L(M) —con base en la función
definida en la figura 6.2 donde se define aquel estado en que se encuentra el autómata, qué
carácter va a ser leído, el elemento a sacar de la pila, el nuevo estado al cual va a pasar y
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
L={c,aca,bcb,aacaa,abcba,bacab,bbcbb,…}
La gramática es:
L={ξ,aa,bb,aaaa,abba,baab,bbbb,…}
La gramática es:
L={ξ,a,b,aa,bb,,aaa,bbb,aba,bab,aaaa,abba,baab,bbbb,…}
Esta sería la definición de un palíndromo, es decir, aquella palabra o expresión que se lee
igual de izquierda a derecha y viceversa. Por ejemplo, en lenguaje español la siguiente ora-
ción forma un palíndromo:
Hasta este momento se ha visto que a partir de una gramática recursiva izquierda se puede
construir un árbol sintáctico utilizando las prioridades relativas de los operadores aritmé-
ticos. Para la construcción automática del árbol se requiere un autómata tipo pila. Ahora
bien, utilizado la idea de equivalencia se puede construir un algoritmo para producir el árbol
sintáctico a partir de una gramática libre de contexto recursiva izquierda que permita trans-
formar la gramática en un autómata tipo pila.
El árbol sintáctico se puede construir de dos formas. Lo común es construir el árbol a partir
de la raíz a las hojas (en forma descendente o top-down). La otra forma es antinatura, esto
es, construir el árbol de las hojas a la raíz (en forma ascendente o bottom-up).
(p,ξ,ξ)(q,S)
Tabla 6.2. Transiciones para la cadena aabcbaa utilizando el autómata del ejemplo 6.5
La tarea de reconocer a un lenguaje libre de contexto en una forma determinística que pro-
duce el árbol sintáctico es algo sumamente importante y es un reto. A un autómata tipo
pila determinístico se le conoce como analizador. Naturalmente, no todos los lenguajes
libres de contexto pueden tener un analizador.
Un analizador descendente es aquel autómata tipo pila determinístico que crea un árbol
89
sintáctico de raíz a las hojas. Tal analizador se logra observando el siguiente elemento para
ser analizado, y dependiendo de ello se realiza la transición.
Su gramática es conocida.
M1=({p,q},{a,b},{a,b,s},Δ,p,{q})
donde Δ={(p,ξ,ξ)(q,S), (q, ξ,S)(q,aSb), (q, ξ,S)(q, ξ), (q,a,a)(q, ξ), (q,b,b)(q, ξ)}
Para observar hacia adelante (look ahead) se deben introducir más estados que permitan
hacer un autómata determinístico, donde se pueda decidir con el símbolo de la cadena por
leer y el símbolo que está en tope de pila la transición que se debe realizar. Además, se
requiere un símbolo especial que indique el fin de cadena ($), por lo que la palabra, en este
caso, queda como w$. Así, el analizador descendente queda de la siguiente forma:
90 En la tabla 6.3 se realizará una simulación con la siguiente cadena: ab$
Tabla 6.3. Usando el analizador descendente del ejemplo 6.6, se analiza la cadena ab$
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
No todo lenguaje libre de contexto tiene un autómata determinístico descendente vía una
observación hacia adelante, incluso para lenguajes libres de contexto no ambiguos. Algunos
lenguajes no son amigables para hacer un analizador descendente por razones que son
superficiales y pueden ser removidas con pequeños cambios en la gramática.
Ejemplo 6.7. Recordemos el lenguaje libre de contexto que representa a un álgebra ele-
mental con los operadores de agrupación, la multiplicación y la suma, siendo esta gramática
recursiva izquierda:
Sea A→Aα1,..., A→Aαn, y A→β1,..., A→βm son todas las reglas con A al lado izquier-
do de la producción y las β’ no inician con A y n>0. Entonces, reemplazar estas reglas
por A→β1A’,..., A→βmA’, A’→α1A’,..., A’→αnA’, y A’→ξ, donde A’ es una nueva no terminal
(Lewis y Papadimitriou, 1998).
Existen varios algoritmos para transformar una gramática libre de contexto a un autómata
tipo pila, pero su coste computacional es elevado (del orden de n3). Por lo tanto, existen
algoritmos más eficientes para ciertas gramáticas libres de contexto. En la siguiente sección
se observará un algoritmo que permite crear una tabla sintáctica para una gramática no
recursiva izquierda.
Una gramática cuyo tabla analizadora no tenga entradas de definición múltiple se dice que
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
Un analizador predictivo dirigido por una tabla tiene una entrada, una pila, una tabla anali-
zadora y una cadena de salida. La entrada contiene a la cadena que debe ser analizada,
seguida por un $ indicando fin de cadena. La pila contiene una secuencia de símbolos de
la gramática. El primer símbolo por introducir a la pila es $, seguido del símbolo no terminal
de inicio. La tabla analizadora es una tabla bidimensional M[A,a], donde A es una letra del
abecedario no terminal y a es una terminal o el símbolo $.
El analizador es controlado por un programa que se comporta con el siguiente algoritmo:
95
El programa considera a X el símbolo en el tope de la pila, y a el símbolo de la entrada a
analizar. Estos dos símbolos determinan la acción que va a realizar el analizador. Existen
tres posibilidades:
Del ejemplo 6.7 se obtuvo una gramática no recursiva predictiva izquierda. Su tabla analiza-
dora se muestra a continuación:
En la tabla 6.7 se realizará una simulación con la cadena de entrada id*(id+id)$, el algoritmo
que permite realizar las transiciones apropiadas a partir de la tabla analizadora y la pila.
Obsérvese que al inicio el stack contiene el $ y el símbolo no terminal de inicio E.
96 Tabla 6.7. Simulación de la tabla 6.6 y la cadena id*(id+id)$
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
En la tabla 6.8 se muestra otro ejemplo con la cadena ((id0*id1+id2)*id3)+id4$:
97
Tabla 6.8.
Para crear una tabla analizadora descendente predictiva se requieren dos pasos:
1. A partir de un nodo en general del árbol sintáctico, se trata de saber cuáles son
los símbolos terminales que pueden aparecer a la izquierda. A esa función se le
conocerá como FIRST(). A partir de un nodo en general, se tratará de saber cuá-
les son los símbolos terminales que pueden aparecer a la derecha. A esta función
se le conocerá como FOLLOW() (el dominio de estas funciones es el alfabeto de
los símbolos terminales, por lo que se espera que cada función FIRST() o FO-
LLOW() tenga un subconjunto de tales elementos). Véase la figura 6.16.
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
2. A partir de estas dos funciones, se sigue un algoritmo para indicar cuáles son
las derivaciones que se deben obtener en forma determinista utilizando como
información al elemento que se localiza en tope de pila y el símbolo de entrada
por analizar.
Función First( )
Para calcular la función First() se usa el siguiente algoritmo:
Si x es un terminal, entonces First (x) ← { x }
Entendiendo que "ξ" se considera la cadena vacía. Si x →ξ, entonces agregar
ξ al First(x). (First(X) ←{ξ}).
Si X es una no terminal y:
X→Y1 Y2 . . . Yk
Y1→Z1 Z2 . . . ZT
Z1->W1W2..
Entonces First(X) ← First(Y1) ← First(Z1)<-First(W1)
99
Ejemplo 6.8. Para mostrar el algoritmo de la función First() se tomará como ejemplo una
gramática izquierda no recursiva:
E→TE’
E’→+TE’
E’-> ε
T→FT’
Para crear los First() solo se aplican las reglas del procedimiento anterior, desglosando los
resultados:
Del paso 2.
First(E’)←{ ε}
First(T’) ←{ ε}
Ejemplo 6.9. Ejemplo de FOLLOW(). Usando la gramática y el FIRST() del ejemplo 6.8,
encontrar la función FOLLOW().
Ejemplo 6.10. Obtener el FIRST( ) y FOLLOW( ) de la siguiente gramática.
101
Se puede usar el siguiente algoritmo para construir una tabla analizadora predictiva para
una gramática G. La idea detrás del algoritmo es la siguiente: suponga A→α es una produc-
ción con a en el FIRST(α). Entonces el analizador puede expandir A por α cuando el símbolo
de entrada es a. Ocurre una complicación cuando α =ξ, o α ξ. En este caso, A se debe
expandir por α si el símbolo de entrada se localiza en FOLLOW(A), o si el signo $ ha sido
alcanzado y $ se localiza en FOLLOW(A).
Algoritmo:
Tabla 6.11
La transición M[S’,e] contiene dos transiciones S’→ξ y S’→eS, por la transición FO-
LLOW(S’)={e,$} la gramática es ambigua y la ambigüedad se manifiesta en escoger, el else
(e) o cadena vacía. Ya que la tabla contiene entradas multidefinidas, la gramática no es
LL(1).
106 AUTÓMATA TIPO ASCENDENTE (BOTTOM-UP)
La idea es construir el autómata de las hojas a la raíz, método conocido como bottom-up.
Ejemplo 6.14. Con el autómata ascendente no determinístico mostrado arriba hacer la tabla
de transiciones con la cadena id0+id1*id2.
En la tabla 6.12 se muestran las transiciones.
107
Tabla 6.12
Para construir un autómata práctico para L(G) se debe crear un autómata determinístico
que acepte L(G)$. A diferencia de un autómata descendente, no se dará un procedimiento
sistemático, sino que se colocarán las bases heurísticas que permiten tal tarea.
Primero se necesita saber el camino para determinar los dos tipos básicos de movimientos,
esto es:
La decisión entre leer un símbolo de la cadena (Shift) o una reducción (sacar α elementos
de la pila) se realiza entre una relación , conocida como relación de pre-
cedencia. Si (a,b) P, entonces habrá una reducción; de otra forma, habrá un Shift con
“b”. Intuitivamente, (a,b) P significa que existe una derivación a la derecha de la forma
ya que se está reconstruyendo la derivación de la derecha hacia atrás,
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
esto hace sentido reducir la regla A→γa donde se observa que “a” inmediatamente precede
a “b”. Existen caminos sistemáticos para calcular la relación de precedencia para decidir
entre un shift o una reducción.
Ejemplo 6.15. Dada la siguiente gramática, mostrar la tabla de precedencia y realizar una
simulación de su funcionamiento.
La tabla 6.13 solo indica si se realiza una reducción o una lectura, pero no cuál reducción
debe efectuarse. La tabla 6.14 muestra un ejemplo utilizando la tabla 6.3. Las reducciones,
en este momento, se realizan de manera intuitiva.
Tabla 6.14. Simulación del uso de la tabla 6.13 de precedencia
109
Con la tabla de precedencia y al reducir escoger la regla de mayor tamaño (si se tiene en
pila) se puede construir un autómata determinístico que analice L(G)$. Un detalle: el autó-
mata debe censar el final de la pila, por lo que se coloca un símbolo especial # en el inicio de
la pila para cualquier computación. En el ejemplo 6.16 se muestra el autómata ascendente
determinístico para la gramática indicada en el ejemplo 6.15.
110 Ejemplo 6.16. Máquina determinística ascendente mostrando la heurística con operadores
de precedencia y reducción de la regla de mayor tamaño.
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
De esta forma se han mostrado dos reglas heurísticas para crear un autómata determinís-
tico ascendente:
Ejemplo 6.17. Mostrar una simulación con el autómata del ejemplo 6.16.
En la tabla 6.15 se muestra una simulación del funcionamiento del autómata determinístico
ascendente antes descrito.
Tabla 6.15. Simulación con una cadena de caracteres con un autómata
111
ascendente determinístico
El único inconveniente de este método es que se requiere mucho trabajo para construir un
analizador LR a mano para una gramática de un lenguaje de programación. Por eso, se
requieren herramientas especiales como Lex (lexicografical analyzer) y YACC (yet another
compile compiler) que realizan el análisis lexicográfico y el análisis sintáctico (ambos se
pueden interconectar).
• SLR. Simple LR. Es el más sencillo de los tres, pero el que tiene menos po-
der de construcción, ya que no llega a reconocer algunas gramáticas libres
de contexto donde otros métodos llegan a tener éxito.
• LR. Es el algoritmo más poderoso, pero a la vez el más costoso.
• LALR. Look Ahead LR. Tiene un costo intermedio entre los dos anteriores.
Trabaja en la mayoría de las gramáticas de los lenguajes de programación
y, con un poco de esfuerzo, puede ser implementado eficientemente.
ANALIZADOR SLR
La construcción de la tabla analizadora SLR se basa en un autómata finito. El autómata se
construye bajo el concepto de los “ítems” (o símbolo por analizar). Un ítem es una regla de
la gramática que tiene un punto en algún lugar de la producción, por lo que el elemento por
analizar es el que se encuentra a la derecha del ítem.
A→XYZ
Repetir
Para cada conjunto I en C y cada símbolo en la gramática X tal que ir_a (I,X) no esté
vacío y no esté en C, realizar:
La siguiente gramática del tipo SLR o LR(0) se utilizará como ejemplo tanto para la genera-
ción del conjunto de cerradura de ítems como para mostrar el algoritmo para crear una tabla
sintáctica SLR:
114 P={
(0) E’E
(1) E E + T
(2) ET
(3) T T * F
(4) TF
(5) F (E)
(6) F id
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
}
donde E indica expresión, T indica término, F indica factor e id indica identificador.
Con el algoritmo dado se genera el conjunto C(I)={I1, . . . ,I11} de la siguiente forma:
Utilizando la función Ir_a( ), se crea el autómata de la figura 6.18:
115
Figura 6.18. Autómata generado por el algoritmo de los ítems
// El símbolo Sj (Shiftj) indica recorrer al siguiente símbolo a analizar y pasar al j-simo estado
del autómata.
Paso 2a
Se toma en cuenta la transición con elementos terminales del autómata finito. Por lo tanto,
las transiciones son las siguientes:
Paso 2b
Para desarrollar el paso 2b se requiere aplicar la función Follow(); ver ejemplo 6.10, que-
dando el siguiente resultado:
Follow ( E ) ← {$, +, )}
Además, se requiere todo ítem que llegó al final de la producción. A continuación, se mues-
tran los ítems que cumplen con tal condición:
{E’→E.} I1
{E→T.} I2
{T→F.} I3
{F→id.} I5
{E→E+T.} I9
{T→T*F.} I10
{F→(E).} I11
Paso 2c
{E’→E.} I1
M[1,$]=acc.
Del autómata finito se toma en cuenta la transición con elementos no terminales. Por lo tan-
to, las transiciones son las siguientes:
Figura 6.19. Manejo de la pila por medio de una Tabla sintáctica LR(K)
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
donde
De esta forma:
ip.- Al inicio apunta al primer símbolo de la palabra por analizar (w$).
S.- Es el estado del tope de pila.
a.- Es el símbolo apuntado por ip.
Si M[S,a]=Shift S, entonces:
Si no, Si M[S,a]=reduce A→ α.
Si no, Si M[S,a]=acc
Si no
En la tabla 6.17 se muestra un ejemplo utilizando el algoritmo para el uso de la tabla sintác-
El autómata permite crear el árbol de forma ascendente. Cada reducción permite ir creando
una rama o unir ramas hacia la raíz. Por lo tanto, las acciones para crear el árbol se hacen
en cada una de las reducciones. Las reducciones se localizan en las iteraciones 2, 3, 7, 8, 9,
12, 13, 14, 16, 17 y 18 realizándose las conexiones entre ramas de derecha a izquierda. En
la figura 6.20 se observa con claridad este aspecto mostrando cada una de las reducciones.
120 Algorítmicamente hablando, se requiere una pila de apuntadores donde se va guardando
el apuntador a cada una de las ramas creadas, por lo que la figura 6.20 también muestra el
funcionamiento de la pila de apuntadores. Obsérvese que al momento de unir dos ramas,
estas se escogen con respecto a los dos apuntadores que están en tope de pila (véanse las
iteraciones 14 y 17).
Tabla 6.18
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
Figura 6.21. Creación del árbol sintáctico en forma ascendente utilizando las iteraciones
1-3, 6-8, 12-14, 17,18, 21-23, 25,26, 29-31, 33-35 de la tabla 6.16
123
Aho, A and Ullman, J. (1978). Principles of compiler design. Wilmington, Delawere, USA:
Addison-Wesley.
Baliri, S. (2014). Teoría de lenguajes formales: una introduccion para lingüistas. España:
Universidad Autónoma de Barcelona.
Isasi Viñuelas, P., Martínez Fernández, P. y Borrajo Millán, D. (1997). Lenguajes, gramáticas
y autómatas: un enfoque práctico. Madrid: Pearson Educación.
Lewis, H. and Papadimitriou, C. (1998). Elements of the theory of computation. New Jersey:
Prentice Hall.
Moral Callejón., S. (s. f.). Teoría de autómatas y lenguajes formales. Universidad de Grana-
da.
Navarrete Sánchez, I., Cárdenas Viedma, M. A., Sánchez Álvarez, D., Botia Blaya, J. A.,
Marín Morales, R. y Martínez Bejar, R. (2008). Teoría de autómatas y lenguajes for-
males. España: Universidad de Murcia.
Universidad Autónoma del Estado de Hidalgo [UAEH] (16 de mayo de 2019). Función del
analizador léxico. Centro de Innovación para el Desarrollo y la Capacitación en Mate-
riales Educativos. Recuperado de http://cidecame.uaeh.edu.mx/lcc/mapa/PROYEC-
TO/libro32/21_funcin_del_analizador_lxico.html
Introducción
En este anexo se muestra la programación del árbol sintáctico ascendente. Toda la progra-
mación se realizó en lenguaje estándar C para que se pueda recrear en cualquier computa-
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
dora que tenga un compilador C. Para el entendimiento del programa se requieren conoci-
mientos de memoria dinámica y haber tenido un curso de estructuras de datos en el cual se
haya programado al menos un árbol binario.
Analizador lexicográfico
Se observará que las palabras que debe reconocer el analizador lexicográfico son:
Por tanto, el analizador lexicográfico es, en este caso, muy simple y al reconocer una pala-
bra retorna por cada palabra válida un número que representa en este caso un token para el
analizador sintáctico. Se observará que por cada token reconocido se invoca al analizador
sintáctico para ver si dicho token corresponde sintácticamente a una letra de la palabra que
127
analizará el analizador sintáctico.
Para manejar ambas pilas a la vez se utilizó un apuntador bidimensional tipo void:
void * p[2];
Donde:
El apuntador p[0] se encarga de apuntar a la pila que producen las iteraciones que indica la
tabla sintáctica ascendente.
p[1] apunta a una pila de apuntadores que permite crear el árbol ascendente donde cada
elemento en la pila de apuntadores apunta a una rama que permitirá crear el árbol sintáctico
de las hojas a la raíz.
130 En la figura A.3 se muestra la función que permite definir las acciones dependiendo de la
consulta a la tabla sintáctica (si se lee otro carácter de la cadena por analizar o se realiza
una reducción a una regla determinada).
Figura A.5. Función que permite crear un elemento en pila del analizador sintáctico
La función más interesante es reduce() y se muestra en la figura A.6. En tal función, la varia-
ble w es el número de regla para reducir. Se observará que en las reglas 1, 3 y 5 se deben
de reducir seis elementos. En la pila del analizador sintáctico se procede como sigue:
132 • Se coloca un ciclo for para eliminar n elementos de la pila (si el número de
regla es impar, se eliminan 6 elementos de pila; si el número de regla es
par, se eliminan 2 elementos de tope de pila) y se utiliza la función elimina()
para tal propósito.
• Se invoca a la función tope() para observar lo que existe en tope de pila y
se guarda en la variable z.
• Posteriormente se invoca la función crea() dos veces: la primera para intro-
ducir la reducción ‘A’ de la regla A→ α y la segunda para introducir a pila el
elemento indicado por la tabla sintáctica (tsin[z][A]).
• Para las reglas 1 y 3 se invoca la función enlaza(), ya que se tienen que
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
Si es una hoja (el elemento a analizar es una “i”), se crea un nodo para tal elemento, se crea
otro nodo para el elemento ‘F’, se conectan entre sí y se guarda el apuntador de ‘F’ a pila
de apuntadores.
O si la reducción se une a la rama que está apuntando en el tope de pila (observar figura
A.8).
Figura A.8. Formación de un ramal del árbol
Figura A.11. Función que elimina un elemento de pila del analizador sintáctico
138 La función que enseña el contenido en pila del analizador sintáctico se muestra en la figura
A.12.
//pila sintáctica
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
struct nodo{
int dato;
nodo * siguiente;
};
// el árbol
struct nodo1{
char dato;
nodo1 * izq;
nodo1 * der;
};
//pila para crear el árbol
struct nodo2{
void * p;
nodo2 * siguiente;
};
int tope(void*);
void mostrar(void *);
void * elimina(void *);
void *crea(void *, int);
int transforma (char);
int tablasin(int[][9]);
void sintactico(void **, int, int, int[][9]);
void *shift(void*, int, int);
141
void reduce(void**, int , int [][9]);
sintactico(p,edo,y,tsin);
}while (y<5);
if (y==6)
printf(“Error lexicográfico, no se reconoce la palabra=>%c”,caracter);
getchar();
}
// CASO 6 ME
// REPRESENTA A “E”)
z=tsin[z][6]; //Reviso el elemento a introducir a
//tope de pila
p[0]=crea(p[0],z);//introduzco el elemento a
//tope de pila
printf(“llamo a enlaza con +\n”);
p[1]=enlaza(p[1], ‘+’);
}
else if(w==3){ //reduce T->T*F
p[0]=crea(p[0],7);
z=tsin[z][7];
p[0]=crea(p[0],z);
printf(“Llamo a enlaza con *\n”);
p[1]=enlaza(p[1], ‘*’);
}
else{ //REDUCE F->(E)
p[0]=crea(p[0],8);
z=tsin[z][8];
p[0]=crea(p[0],z);
p[1]=creaA(p[1],’F’);
}
}
145
else{
for (int i=0;i<2;i++) //ELIMINO DOS ELEMENTOS
p[0]=elimina(p[0]);
z=tope(p[0]);
printf(“Reducción de dos elementos\n”);
mostrar(p[0]);
putchar(‘\n’);
case ‘)’:x=4;break;
case ‘$’:x=5;break;
default:x=6;
}
return x;
}
int tablasin(int x[][9]){
for(int i=0;i<12;i++)
for (int j=0;j<9;j++)
x[i][j]=-1;
x[0][0]=15; //S5
x[0][3]=14; //S4
x[0][6]=1; // IR AL ESTADO 1
x[0][7]=2; // IR AL ESTADO 2
x[0][8]=3; // IR AL ESTADO 3
x[1][1]=16; //S6
x[1][5]=1000; //ACC
x[2][1]=102; // REDUCE E->T
x[2][2]=17; //S7
x[2][4]=102; // REDICE E->T
x[2][5]=102; // REDICE E->T
x[3][1]=104; // REDUCE T->F
x[3][2]=104; // REDUCE T->F
147
x[3][4]=104; // REDUCE T->F
x[3][5]=104; // REDUCE T->F
x[4][0]=15; //S5
x[4][3]=14; //S4
x[4][6]=8; // IR AL ESTADO 8
x[4][7]=2; // IR AL ESTADO 2
x[4][8]=3; // IR AL ESTADO 3
q->siguiente=NULL;
if(p==NULL) //La pila está vacía
p=q;
else{
q->siguiente=(nodo*)p;
p=q;
}
return(p);
}
//------------------------------------------------
//FUNCIONES PARA CREAR EL ÁRBOL
p=q1;
}
}
//se crea el una parte de la rama del árbol
q=(nodo1*)malloc(sizeof(nodo1));
q->dato=x;
printf(“\n\tGuardo un %c\n\n”,q->dato);
q->der=NULL;
aux1=(nodo2*)p;
aux=(nodo1*)aux1->p;
q->izq=aux;
aux1->p=q;
p=aux1;
return(p);
}
ANÁLISIS LEXICOGRÁFICO
ANÁLISIS LEXICOGRÁFICO Y SINTÁCTICO DE UN COMPILADOR
Y
SINTÁCTICO DE UN COMPILADOR