Lenguajes y Autómatas II - Unidad 2
Lenguajes y Autómatas II - Unidad 2
Lenguajes y Autómatas II - Unidad 2
2.1. Notaciones.
32
traducción a notaciones simples. Esto se logra mediante la verificación de tipos de
datos, nombres de símbolos, uso de ciclos, resultados de funciones y requisitos de
variables. Si se detectan errores, el compilador genera un informe de errores para
informar al usuario y ayudarlo a corregir los errores de manera rápida y eficiente.
2.1.1. Prefija.
33
que la notación infija, ya que los operadores aparecen antes de sus operandos
correspondientes. Esta notación también es más fácil de evaluar para el ordenador, ya
que los operadores se aplican en el orden correcto sin necesidad de usar paréntesis.
Sus características principales son:
2.1.2. Infija.
Es una expresión aritmética escrita de manera tal que cada operación se escribe con
los dos operandos al lado del operador.
Ejemplo: 5 + 3
En esta notación, los dos números se encuentran al lado del operador "+", lo cual
significa que se quiere sumar 5 más 3.
Otro ejemplo: 8 × 5
En este caso, los dos números se encuentran al lado del operador "×", lo cual significa
que se quiere multiplicar 8 por 5.
Esta notación se conoce como notación infija, ya que los operadores se encuentran
entre los operandos.
La notación infija se contrasta con la notación prefija o notación polaca inversa, en la
cual los operadores se encuentran antes de los operandos.
Por ejemplo, en la notación prefija, la operación anterior se escribiría como "× 8 5". La
notación infija es la más comúnmente usada en matemáticas, ya que es la más
intuitiva para la mayoría de los matemáticos.
2.1.3. Postfija.
También hay una notación posfija, en la cual los operadores se encuentran después de
los operandos. Por ejemplo, en la notación posfija, la operación anterior se escribiría
como "8 5 ×".
Estas notaciones se utilizan en computación para facilitar el procesamiento de los
operadores y operandos.
Es una notación para representar expresiones aritméticas en la que los operandos
preceden a los operadores.
Los operandos se escriben antes de los operadores, en lugar de entre paréntesis como
se suele hacer en la notación infija.
2 3 + en notación posfija.
Esta notación es útil para expresiones más complejas que contienen varios
operadores, ya que los operadores se pueden aplicar en el orden correcto sin
necesidad de usar paréntesis.
Por ejemplo
2 3 4 × + en notación posfija.
Esta notación también se conoce como notación polaca inversa o notación polaca
posterior. Se utiliza comúnmente en lenguajes de programación como Lisp, y también
se utiliza para la evaluación de expresiones aritméticas en árboles de expresión.
En general, la notación posfija se considera más fácil de usar que la notación infija, ya
que los operadores no tienen que estar entre los paréntesis. Esta notación también es
más fácil de evaluar para la computadora, ya que los operadores se aplican en el orden
correcto sin necesidad de usar paréntesis.
35
¨La fase de generación de código intermedio se encarga de traducir el programa a
una representación de código intermedio para que luego ésta pueda ser transformada
en las fases subsiguientes a un código ejecutable en una determinada arquitectura
física¨ (Vélez Reyes, Javier).
Figura 1. Vélez Reyes, Javier (2010). Esquema de Generación de Código Intermedio (Diseño).
Procesadores de Lenguajes. Ingeniería Técnica Superior de Ingeniería Informática.
Departamento de Lenguajes y Programas Informáticos. Editorial UNED.
36
Algoritmo:
- Para cada token encontrado:
- si es un operador, tomar los últimos dos elementos de la pila de operandos y
realizar la operación correspondiente. Después de la operación, el resultado se
coloca en la pila de operandos.
- si es un operando, se coloca en la pila.
Al final, la pila debería contener un solo elemento, el resultado de la operación.
Si la pila contiene más de un elemento, esto significa que el número de operadores no
fue el correcto para el número de operandos.
Por ejemplo, para compilar la expresión "2 + 3 * 5", el compilador debería realizar los
siguientes pasos:
1. Leer el token "2": se coloca en la pila de operandos.
2. Leer el token "+": tomar los últimos dos elementos de la pila (2 y 3) y realizar la
operación de suma. El resultado se coloca en la pila.
3. Leer el token "*": tomar los últimos dos elementos de la pila (5 y 7) y realizar la
operación de multiplicación. El resultado (35) se coloca en la pila.
4. La pila contiene un solo elemento, el resultado de la operación (35). El compilador ha
terminado de compilar la expresión.
En general, la notación polaca es una forma útil de escribir expresiones matemáticas
para que sean fácilmente compiladas por un compilador. Esto se debe a que no hay
ambigüedad en la forma en que se escriben las operaciones. Además, al leer el código
fuente de izquierda a derecha, el compilador puede realizar la operación
correspondiente sin tener que preocuparse por el orden de las operaciones. Esto hace
que sea más fácil para el compilador procesar la expresión y generar el código de
salida correcto.
Algoritmo para la notación polaca generada a través de un autómata con pila.
▪ Representar la expresión en forma de árbol sintáctico.
▪ Recorrer el árbol en postorden
Ejemplo: a + b * c-d
37
Figura 2. Árbol de expresión para la expresión a + b * c-d. Código a b c * + d-
2.2.2. Código P.
38
subyacente. Esto significa que el código intermedio P puede ser traducido a código de
máquina para cualquier plataforma sin necesidad de reescribir todo el código.
2.2.3. Triplos.
Por ejemplo, una tripleta podría ser: sumar, x, y; lo que significa que el valor de la
variable ¨x¨ se sumará con el valor de la variable ¨y¨ y el resultado se almacenará en la
variable ¨y¨.
El compilador también puede simplificar este proceso para hacer que sea más
eficiente, como reemplazar los nombres de variables por números o agrupar
instrucciones para reducir la cantidad de tripletas necesarias.
Las tripletas pueden utilizarse para crear una tabla de símbolos para mantener un
seguimiento de las variables y los valores asignados a ellas. Esta tabla es útil para el
compilador para realizar tareas como la verificación de tipos y generar código para la
máquina.
39
Esta tabla contendrá tripletas con los nombres de las variables, sus tipos y sus valores.
Esto le permitirá al compilador seguir el flujo de datos a través del programa y verificar
que los tipos de datos sean correctos.
Las tripletas también son utilizadas por el compilador para generar código en
lenguaje de máquina. Esto significa que el compilador convertirá las tripletas en
instrucciones máquina que puedan ejecutarse por la computadora.
Estas instrucciones son diferentes para cada tipo de computadora y se pueden crear
un conjunto específico para cada computadora. Esto significa que el compilador
tendrá que generar el código para cada computadora específica en la que se desea
ejecutar el programa.
En la compilación, una tripleta se representa como un conjunto de instrucciones de
tres direcciones, una para cada uno de los tres componentes de la tripleta. Estas
instrucciones están diseñadas para realizar operaciones con los tres componentes de
la tripleta para producir los resultados deseados. Por ejemplo, una tripleta que
represente una suma se representará como un conjunto de instrucciones que tomen
los dos operandos, los sumen y guarden el resultado en una ubicación de memoria.
Estas instrucciones se llaman instrucciones de tripleta y se usan para representar
cualquier cosa que un programador desee hacer con los operandos. En algunos
lenguajes de programación, como C, el compilador generará código de tripleta para
representar cada tripleta. Esto significa que cada instrucción se traduce a un conjunto
de instrucciones de tripleta para realizar la misma operación. Esto ayuda a simplificar
la compilación y hace que el código generado sea más eficiente.
Ejemplo Triplos
<operador> <operando1> <operando2>
Ejemplo: W * X + (Y + Z)
1. (*, W, X)
2. (+, Y, Z)
3. (+, (1), (2))
Control de flujo:
IF X>Y
THEN Z=X
ELSE Z=Y+1
1. (>, X, Y)
2. (Saltar si falso, (1), (5) )
3.( =, Z, X)
4. (Saltar,, (7))
5. (+, Y, 1)
6. (=, Z, (5))
7.- RET
40
El resultado se asocia al número de tripleta, cada tripleta se enumera con un número
consecutivo. Al hacer referencia a los números de cada tripleta se encierran en
paréntesis, para diferenciarlos de un número que está siendo usado como operando.
Como podemos ver, una tripleta es un conjunto de tres elementos que describen una
instrucción de un programa. Estas tripletas se usan para generar código intermedio,
que es un lenguaje que usan los lenguajes de programación para comunicarse entre
sí. Esto significa que el código intermedio se usa para traducir el código fuente escrito
por el programador en un lenguaje de programación particular a un lenguaje de
programación diferente. El código intermedio se compone de instrucciones de
lenguaje de máquina simples, como instrucciones de suma, resta, multiplicación y
división. Estas instrucciones se expresan como tripletas, que contienen los operandos,
las instrucciones y el resultado.
Las tripletas se usan para generar el código intermedio, que luego se compila para
generar el código de lenguaje de máquina.
El uso de tripletas para generar código intermedio permite a los programadores
escribir código fuente en cualquier lenguaje de programación y que el compilador lo
traduzca directamente a lenguaje de máquina. Esto hace que la codificación de
programas sea mucho más fácil y rápida. Estas tripletas se usan para generar código
intermedio, que es un lenguaje que usan los lenguajes de programación para
comunicarse entre sí.
Aquí tenemos un ejemplo de cómo podríamos generar una tripleta desde una
expresión "A + B" en Python:
class Tripleta:
def __init__(self, operador, operando1, operando2):
self.operador = operador
self.operando1 = operando1
self.operando2 = operando2
def generar_tripleta(expresion):
operadores = ['+', '-', '*', '/']
# Ejemplo de uso
expresion = "A + B"
tripleta = generar_tripleta(expresion)
print("Operador:", tripleta.operador)
print("Operando1:", tripleta.operando1)
print("Operando2:", tripleta.operando2)
41
En este ejemplo, se ha definido una clase Tripleta para representar la tripleta
resultante y una función generar_tripleta que toma una expresión y busca uno de los
operadores permitidos ('+', '-', '*', '/') en ella. Luego, se divide la expresión en dos
operandos y se crea una instancia de la clase Tripleta con el operador y los operandos
encontrados.
Hay que tener en cuenta que este ejemplo es muy simple y asume que la expresión
contiene un solo operador binario. Para expresiones más complejas con varios
operadores, paréntesis y precedencia de operadores, se necesita una implementación
más sofisticada que analice la expresión de manera más completa.
2.2.4. Cuádruplos.
42
Esto le permite al compilador detectar errores en tiempo de compilación antes de que
el código se ejecute, ayuda a mejorar la calidad del código y reduce el tiempo de
depuración.
Un cuádruplo es una estructura de datos compuesta por cuatro elementos: operador,
dos operandos y un resultado.
Los cuádruplos se usan para representar instrucciones en lenguajes de programación.
Por ejemplo, la instrucción "x = y + z" se puede representar en un cuádruplo como (+, y,
z, x).
El primer elemento es el operador (+) y los siguientes elementos son los operandos (y,
z). El último elemento es el resultado (x). El cuádruplo indica que se debe sumar los
valores de y y z, y luego asignar el resultado a la variable x.
Esta representación intermedia es una forma de codificar instrucciones de manera
compacta y clara.
Ejemplo:
<operador> <operando1> <operando 2> <destino>
(A+B)*(C+D)-E
1. (+, A, B, T1)
2. (+, C, D, T2)
3. (*, T1, T2, T3)
4. (-, T3, E, T4)
43
4. Un conjunto de herramientas de generación: Estas herramientas permiten a los
programadores generar oraciones de manera aleatoria a partir de un esquema de
generación.
Para generar cadenas de texto a partir de una gramática, se puede seguir el siguiente
esquema:
a) Definir una gramática formal: La gramática formal debe especificar las reglas
para construir cadenas de texto. Para ello, se pueden definir símbolos
terminales y no terminales, producciones, reglas de derivación, entre otros
elementos.
b) Identificar el símbolo inicial: El símbolo inicial es el punto de partida para la
generación de cadenas de texto. En general, se utiliza un símbolo no terminal
como símbolo inicial.
c) Aplicar las reglas de derivación: Las reglas de derivación indican cómo se
pueden transformar los símbolos no terminales en otros símbolos terminales y
no terminales. Para generar una cadena de texto, se puede aplicar una
secuencia de reglas de derivación partiendo del símbolo inicial hasta que se
obtengan únicamente símbolos terminales.
d) Repetir el paso anterior para generar más cadenas: Una vez que se ha generado
una cadena de texto, se puede repetir el proceso para obtener más cadenas
siguiendo los mismos pasos.
44
El proceso de código intermedio para variables y constantes es el proceso de
traducción de instrucciones de lenguaje de alto nivel en instrucciones de lenguaje de
bajo nivel. Se realiza mediante una serie de pasos, comenzando con la identificación
de variables y constantes. Una vez que se hayan identificado estas variables y
constantes, el siguiente paso es asignarles una dirección de memoria. Esto permite
que el compilador sepa dónde buscar los datos cuando se ejecute el código. A
continuación, el compilador comenzará a generar instrucciones de código de bajo
nivel para cada línea de código de alto nivel. Estas instrucciones traducirán la lógica
del programa en instrucciones que el procesador pueda entender.
Finalmente, se compilará el código intermedio, lo que generará un archivo ejecutable,
listo para ejecutarse en el procesador.
2.3.2. Expresiones.
45
TemporalFactory tF = new TemporalFactory (scope);
IntermediateCodeBuilder cb = new …Builder (scope)
TemporalIF temp1 = e1.getTemporal ();
TemporalIF temp2 = e2.getTemporal ();
TemporalIF temp = tF.create ();
cb.addQuadruples (e1.getCode ());
cb.addQuadruples (e2.getCode ());
cb.addQuadruple (“ADD”, temp, temp1, temp2);
e.setTemporal (temp);
e.setCode (cb.create());
RESULT = e
:}
x = y + z;
Siguiendo el esquema, se realizarían las siguientes acciones:
46
3. Se verifica que la variable "x" existe y tiene un tipo de dato compatible con el
resultado de la suma de "y" y "z". Suponiendo que todas estas variables son de
tipo entero, la asignación es válida.
4. Se genera el código correspondiente para realizar la suma de "y" y "z" y asignar
el resultado a "x":
Mov r1, y
add r1, z
mov x, r1
El código exacto puede variar dependiendo del lenguaje de programación y la
arquitectura del procesador utilizado.
En términos generales, la traducción de una instrucción de asignación en la
compilación involucra los siguientes pasos:
47
Con el proceso anterior se genera una representación en código intermedio de la
instrucción de asignación. Este código intermedio es independiente del lenguaje de
programación y de la arquitectura de la computadora y es una representación del
código fuente. Deben representar la secuencia de operaciones para que se lleve a
cabo la asignación. Esto es las operaciones para que se copie el valor del operando
derecho al operando izquierdo, en la ubicación de memoria que el compilador le
asignó.
Por ejemplo, si consideramos la siguiente instrucción de asignación en el lenguaje de
programación C:
x = y + z;
<<comprobación de tipos>>
48
cb.addQuadruple (“ADD”, temp, temp, rTempO);
2.3.4.Instrucciones de control.
La generación de código intermedio para una instrucción de control depende del tipo
de instrucción de control que se esté considerando. A continuación, se muestran
algunos ejemplos de generación de código intermedio para diferentes tipos de
instrucciones de control:
t1 = a > b;
if t1 goto label1;
En este caso, el valor booleano de la expresión "a > b" se almacena en la
variable temporal "t1". Si el valor de "t1" es verdadero, se salta a la etiqueta
"label1".
3. Instrucción de retorno: Una instrucción de retorno devuelve un valor de una
función y sale de ella. El código intermedio generado para una instrucción de
retorno depende del valor de retorno y de si la función tiene un tipo de retorno.
Supongamos que tenemos la siguiente instrucción de retorno: "return a + b;". El
código intermedio generado para esta instrucción podría ser:
t1 = a + b;
return t1;
49
1. Evaluación de la expresión condicional: En primer lugar, se evalúa la expresión
condicional de la instrucción de control, para determinar si se debe o no
ejecutar el bloque de código asociado.
2. Generación de código para el bloque de código: Si se debe ejecutar el bloque
de código, se genera el código intermedio correspondiente para ese bloque.
Esto puede implicar la generación de código para una serie de instrucciones
individuales, cada una de las cuales realiza una tarea específica.
3. Generación de código de salto: Después de generar el código para el bloque de
código, se genera un código de salto para saltar a la siguiente instrucción
después del bloque de código, o para volver al inicio del bucle en el caso de una
instrucción de bucle.
4. Actualización de las direcciones de salto: Si se usaron saltos para implementar
la instrucción de control, se debe actualizar la dirección de salto en la
instrucción de salto para apuntar a la ubicación correcta en el código
intermedio.
2.3.5. Funciones.
50
intermedio y generación de código de máquina. Cada paso es esencial para crear un
código de máquina eficiente y funcional a partir del código fuente original.
Función en C:
return a + b;
En este seudocódigo, hemos definido una estructura Cuadrupla para representar las
cuádruplas y una lista cuadruplas para almacenarlas. La función agregarCuadrupla se
utiliza para agregar cuádruplas a la lista.
51
Luego, hemos definido una función suma que realiza una suma y genera una
cuádrupla correspondiente. En el ejemplo de llamada a función, asignamos valores a x
y y, y luego llamamos a la función suma(x, y) para calcular la suma y almacenar el
resultado en z.
52
Aquí tenemos un seudocódigo que muestra cómo podríamos representar funciones y
llamadas a funciones utilizando cuádruplas en un contexto de compilación:
Luego, se ha definido una función suma que realiza una suma y genera una cuádrupla
correspondiente. En el ejemplo de llamada a función, se asignan valores a ¨x ¨y ¨y¨, y
luego se llama a la función suma(x, y) para calcular la suma y almacenar el resultado
en z.
53
Finalmente, después de ejecutar la llamada a función, se imprimen las cuádruplas
generadas.
2.3.6. Estructuras.
54
Estos son solo ejemplos de cómo se podría generar código intermedio para una
estructura durante la compilación. El código real generado dependerá del lenguaje
fuente, la arquitectura de la máquina objetivo y otros factores.
Generar cuádruplas para estructuras de control, como ciclos o bucles y condicionales,
en un compilador implica mantener un seguimiento de las etiquetas de inicio y
finalización de las estructuras, así como realizar saltos condicionales o incondicionales
según sea necesario. A continuación, se presenta un seudocódigo básico que muestra
cómo se podrían generar cuádruplas para un bucle while:
55
En este seudocódigo, se ha definido una estructura Cuadrupla para representar las
cuádruplas y una lista cuadruplas para almacenarlas. La función agregarCuadrupla se
utiliza para agregar cuádruplas a la lista.
Para representar un bucle while, se han utilizado etiquetas (ETIQUETA) para marcar el
inicio y el final del bucle. El bucle se ejecuta mientras la condición sea verdadera y se
evalúa en cada iteración. Cuando la condición se vuelve falsa, se genera un salto
condicional de regreso al inicio del bucle.
56