Está en la página 1de 17

Unidad VIII

Implementación de LP.

Procesamiento de LP: diseño e implementación.

Debido a que las computadoras solo pueden interpretar y ejecutar código máquina, es necesario contar con programas que
permitan traducir programas escritos en lenguajes de programación a lenguaje máquina. Existen lenguajes de bajo y alto nivel.
• Los LP de bajo nivel tienen una mayor velocidad de ejecución al estar cercanos a la arquitectura de la maquina, sin
embargo por esta misma razón carecen de portabilidad, y además las instrucciones están bastante alejadas del lenguaje
humano y tienen poco poder expresivo.
• En cambio, los LP de alto nivel tienen un mayor nivel de abstracción, son fáciles de leer y aprender y de escribir y son
mas confiables. (El software se considera confiable si se comporta como es anunciado y produce los resultados que el
usuario espera. Cuando se presenta un error, debería ser fácilmente detectado y corregido).
Por lo que es aquí en donde se genera el “conflicto”, para la máquina es conveniente trabajar con lenguajes de bajo nivel y el
usuario con lenguajes de alto nivel. Entonces (quienes diseñan LP) deben tratar de minimizar la diferencia entre la necesidad
de la maquina y la del programador. (Y para esto están los implementadores, que se encargan del desarrollo de intérpretes o
compiladores, para poder realizar la traducción de programas a lenguaje máquina.
• Los diseñadores se encargan de las estructuras legales de los programas y de especificar reglas de alcance,
precedencia, asociatividad, Etc., NO especifican como debe hacerse la implementación.
• Los implementadores se encargan del desarrollo de intérpretes o compiladores para poder realizar la traducción de
programas a lenguaje máquina, respetando las especificaciones de los diseñadores.

Intérpretes y compiladores.

Intérprete: es un programa que chequea y ejecuta cada instrucción en el momento en que la encuentra. Es decir, traduce y
ejecuta. Para cada instrucción del lenguaje existe un subprograma escrito en lenguaje máquina equivalente. El intérprete mapea
de la instrucción de alto al subprograma de bajo nivel. Algunos lenguajes que son interpretados: APL, PROLOG, LISP, PHP y
JavaScript.
• Ventajas: fácil depuración (búsqueda de errores), ya que la ejecución puede interrumpirse fácilmente, modificarse y
ser ejecutado nuevamente. Es decir, que facilita el desarrollo de programas, ya que el compilador no permite la
ejecución paso a paso, en cambio el interprete si, facilitando así la depuración. Mayor flexibilidad.
• Desventajas: Hay que interpretar cada vez que se quiere ejecutar. Y es mas lento y consume mas y recursos que un
codigo compilado (pues el intérprete ocupa tiempo y memoria) ← Los interpretados usan ligadura dinámica; aparte de
eso, deben guardar la tabla de símbolos en ejecución para hacer controles (lo cual ocupa espacio).

Compilador: traduce un programa escrito en lenguaje de alto nivel a uno equivalente en lenguaje máquina (o ensamblador)
pero no lo ejecuta, sino que lo guarda en disco y queda listo para ser ejecutado. Además el compilador realiza algunas
optimizaciones sobre el código, como eliminar variables y constantes que no utilizadas, o ciclos que estén de más, de manera
que ocupe menos espacio y sea más rápido de ejecutar. Algunos lenguajes que son normalmente compilados: Pascal, C, C++,
Ada, FORTRAN, COBOL, Modula-21. En general, primero se traduce a código intermedio y luego a código máquina.

1 Java es especial: por que se compila en bytecode, y luego ese bytecode se interpreta con la JVM. Así que técnicamente es un híbrido, aunque de cualquier
manera, es posible generar código máquina real con Java (aunque no vale la pena por que penalizás la portabilidad que pondera Java).

1
Básicamente la traducción por compilación se divide en dos fases:
• Análisis del programa fuente de entrada, que seria las fases de reconocimiento del programa fuente (marcada en la
imagen con el arco naranja). Para el traductor, el programa fuente se presenta inicialmente como una serie larga de y
no diferenciada de caracteres. A pesar de que el código fuente esté dividido en módulos el traductor no nota,
inicialmente, este tipo de divisiones.
◦ Análisis léxico: realiza un análisis lineal; la cadena de entrada se lee de izquierda a derecha, y se la descompone
en componentes léxicos (tokens) y se determina de que tipo es cada elemento léxico (por Ej., si es número,
identificador, delimitador, operador, Etc.) adjuntando una marca de tipo, para proveer estos elementos a las etapas
superiores del traductor. Luego se suele hacer una conversión a una representación interna de los elementos. Por
Ej., los números se convierten a una forma binaria interna con punto fijo o flotante; los identificadores se guardan
en una tabla de símbolos y se usa la dirección de la entrada de la tabla de símbolos en lugar de la cadena de
caracteres (que servirá para alimentar las etapas posteriores de traducción). Las unidades resultantes de dicho
análisis se denominan unidades o componentes léxicos. En esta etapa también se eliminan los comentarios
innecesarios del código, y todos aquellos elementos que solo sean relevantes para el usuario y no para el
programa en sí (como las palabras pregonadas, los espacios en blanco que no significan nada, Etc.). Es la etapa
que, en proporción, lleva más tiempo debido a que el análisis se realiza carácter por carácter. 2
◦ Análisis sintáctico (parsing): Partiendo de lo que ha recibido del analizador léxico, la tarea del analizador
sintáctico consiste en ir descubriendo las estructuras (enunciados, expresiones, subprogramas, Etc.) presentes en
el código. Es decir, en esta etapa, se identifican estructuras más complejas, agrupando los componentes léxicos
volcados en la tabla de símbolos por la etapa anterior. A partir de las estructuras que ha encontrado, el analizador
sintactico construye un arbol sintactico (una arbol de analisis sintactico define la estructura jerarquica del
programa, y está basado en las reglas formales descriptas en la GLC -mediante BNF, por ejemplo-).
▪ El analizador sintáctico trabaja en forma conjunta con el analizador semántico y se comunican a través de
una pila. El analizador sintáctico vuelca en la pila, los datos a analizar y el semántico los recupera y procesa. 3
◦ Análisis semántico: a partir de las estructuras sintácticas, busca información de tipos y detecta errores
semánticos (verificación de tipos, correspondencia de nombres, Etc.), por ejemplo, si los operandos y operadores
no son compatibles, y debe generar algún mensaje de error y continuar. Se obtiene como resultado código objeto
intermedio/sencillo, que luego puede ser sometido a la etapa de optimización para eliminar redundancias.
▪ Basicmanete busca errores semánticos y reúne la información sobre los tipos para la fase posterior de
generación de código. En ella se utiliza la estructura jerárquica determinada por la fase de análisis sintáctico
para identificar los operadores y operandos de expresiones para finalmente generar el código intermedio.
▪ Un componente importante del análisis semántico es la verificación de tipos. Aquí, el compilador verifica si
cada operador tiene operandos permitidos por la especificación del lenguaje fuente. (Si detecta error debe
generar un mensaje de error, y tratarlo de alguna manera que permita continuar con el analisis)

2 Nico la nerdeó feo tirando: "la herramienta que se utiliza para llevar a cabo esta tarea es el autómata de estados finitos”, basándose en las filminas de
AyLF. Un autómata finito (AF) o máquina de estado finito es un modelo computacional que realiza cómputos en forma automática sobre una entrada para
producir una salida. Este modelo está conformado por un alfabeto, un conjunto de estados y un conjunto de transiciones entre dichos estados. Su
funcionamiento se basa en una función de transición, que recibe a partir de un estado inicial una cadena de caracteres pertenecientes al alfabeto (la
entrada), y que va leyendo dicha cadena a medida que el autómata se desplaza de un estado a otro, para finalmente detenerse en un estado final o de
aceptación, que representa la salida. La finalidad de los autómatas finitos es la de reconocer lenguajes regulares, que corresponden a los lenguajes
formales más simples según la Jerarquía de Chomsky.
3 El An. Sintáctico trabaja con las reglas sintácticas del lenguaje (GLC), y el An. Semántico trabaja con la Gramática de Atributos (que trabaja con aspectos
sensibles al contexto, pero es estática, es decir, puede probarse en tiempo de compilación)

2
▪ Toma las estructuras sintácticas de la pila, las procesa y empieza a generar el código objeto.
▪ Una de las tareas importantes (ademas de la verificacion de tipos) que se hacen en esta etapa, es la de
mantener la tabla de símbolos actualizada y anexarle información adicional útil para la ejecución del
programa. Algunos datos que se insertan son:
• Si un identificador es un nombre de arreglo, variable simple, nombre de subprograma, Etc., adjuntando
una marca de tipo (es decir, a que tipo de estructura corresponde el identificador)
• Ambiente de referenciamiento.
• Tipo de valores de los identificadores, enteros, reales, Etc.
• Cualquier información adicional que mejore la ejecución.
• La tabla de símbolos se puede desechar al final del proceso de traducción o se puede mantener para
permitir la creación de nuevos identificadores en ejecución o para ayudar en el proceso de depuración.
Además toda información implícita, debe quedar explicitada para los programas de más bajo nivel.
▪ Además, debe estar preparado para detectar errores. Los analizadores sintácticos y semánticos deben estar
preparados tanto para manejar programas correctos como incorrectos.
▪ El An. Semántico está compuesto por varios analizadores más chicos, cada uno con una función en
particular.
▪ Otra función del AS es la de expandir macros 4, y también la de insertar información implícita (como las
declaraciones en variables definidas implícitamente, ponele).

• Síntesis del programa objeto ejecutable, que seria las fases de generación del código objeto (marcada en la imagen
con los arcos amarillos).
◦ Optimizador: Esta fase es opcional, cabe destacar. La fase de optimización de código consiste en mejorar el
código intermedio (generado por el analizador semántico), de modo que resulte un código máquina más rápido de
ejecutar o que ocupe menos lugar en memoria. Esto se hace mediante técnicas como sustitución de invariante,
factorización, reemplazo de rutinas repetidas... Existen compiladores que hacen mucha optimización, llamados
«compiladores optimizadores»; una parte significativa del tiempo del compilador se ocupa en esta fase. Sin
embargo, hay optimizaciones sencillas que mejoran sensiblemente el tiempo de ejecución del programa objeto sin
retardar demasiado la compilación5.

◦ Generación del código: una vez optimizado, se transforman los enunciados a lenguaje ensamblador o máquina, y
se obtiene finalmente el programa objeto. Es decir, es la fase del compilador que se encarga de generar las
instrucciones en código objeto o intermedio .

Finalizadas las fases de Análisis y Síntesis, suele venir la de Vinculación y Carga. Los fragmentos de código que son
resultado de traducciones individuales de subprogramas se juntan en el programa final ejecutable. El cargador o editor de
enlaces carga los diversos segmentos de código traducidos en la memoria y luego los vincula entre si usando una tabla anexa.
Un editor de enlace o ligador es un programa que toma uno o más objetos generados por un compilador y los combina en un
solo programa ejecutable (o sea, tiene el rol de mapear las direcciones de memoria relativas de cada uno de los módulos, en
una sola tabla, para que al momento de cargar el programa en memoria no surjan conflictos). //Por ejemplo, los módulos en
Modula-2 se compilan de forma separada//
---
Ventajas y desventajas: un intérprete tiene como ventajas que permite la implementación fácil de muchas operaciones de
depuración y los errores se marcan en la línea de origen. Las desventajas son que la ejecución es mas lenta que la de un sistema
compilado y que requiere mas espacio (en memoria principal), ya que la tabla de símbolos debe estar presente durante la
interpretación. El compilador tiene la ventaja de que la ejecución es mas rápida y que la decodificación se realiza una sola vez.
La desventaja es que el traductor es grande y complejo.
---

4 Una macro es una porción de código que se compila por separado. Al encontrarse una macro, se pone en suspenso el resto del análisis del programa
principal, y se analiza esa macro.
5 Según Carabio, la semántica operacional colabora al momento de implementar optimizaciones, por que permite ver el funcionamiento de rutinas (por
ejemplo) a bajo nivel, y notar (por ejemplo) la invariante se puede extraer.

3
Híbridos. Otra forma de un implementar un lenguaje de alto nivel, además del interprete y el compilador, es mediante
implementación híbrida. Los sistemas híbridos traducen los programas escritos en lenguajes de alto nivel a un lenguaje
intermedio, que permita una fácil interpretación. Es decir, traduce un programa fuente en un programa objeto, que sirve como
entrada a un interprete. Los sistemas híbridos son mas rápidos que la interpretación pura, ya que las sentencias son
decodificadas una sola vez. Sin embargo, requiere de un intérprete. Algunos lenguajes híbridos son Java y Perl.

• El proceso de una implementación híbrida es el siguiente: al recibir el programa fuente realiza el análisis léxico, tal
como en el compilador, donde se obtienen los componentes léxicos. Luego se hace el análisis sintáctico que toma los
componentes léxicos y genera el árbol de análisis sintáctico. Después interviene el generador de código intermedio,
cuya salida es el código intermedio que se le pasa al interprete, que también recibe los datos de entrada para producir
los resultados.

Procesador semántico abstracto.

La semántica de un LP puede ser descripta especificando el comportamiento de un procesador abstracto que ejecuta programas
escritos en el lenguaje. Las construcciones de un LP pueden ser ejecutadas por una secuencia de operaciones de un procesador
abstracto denominado SimpleSem que modela la semántica de las construcciones.
• SIMPLESEM entonces nos permite definir y entender la semántica de las construcciones de los LP.
• El objetivo es mostrar como las construcciones del lenguaje pueden ser ejecutadas por secuencias de operaciones
del procesador abstracto.
• La semántica del lenguaje de programación puede ser descripta por reglas que especifican como cada construcción del
programa es traducida a una secuencia de instrucciones equivalentes Simplesem.
• SIMPLESEM consiste de un puntero de instrucción (que indica que instrucción se está ejecutando y se denomina ip),
una memoria que se divide en dos secciones separadas, una para el Código y otra para los Datos (registro de
activación) (y a cada una posición de memoria como se la referencia asi: C[i] o D[i]), y un procesador que actualiza
el área del código, modifica la de datos (los datos son los que le damos de entrada -variables-) y el puntero.

• Para que la máquina arranque, se carga un programa en memoria (mediante un programa cargador) y se hace apuntar
ip a la primera instrucción del programa.
• La máquina opera ejecutando los siguientes pasos repetidamente hasta que ejecuta una instrucción especial de stop (o
Halt: detener):
◦ Primero: Ejecuta la instrucción apuntada por el ip (por el puntero).
◦ Luego: Si la instrucción no modifica explícitamente el puntero (ip), luego el ip se incrementa para apuntar a la
próxima instrucción a ser ejecutada (ip:=ip+1). Si ha sido modificado explicitamente por la instruccion que se
ejecutó, entonces ejecuta la instrucción a la que apunta (Como aparece en el ejemplo en la línea del While, más
adelante).

4
• Suponiendo que SIMPLESEM tiene instrucciones para las operaciones del lenguaje en el cual fué escrito el programa,
las operaciones basicas serán del tipo de Modificación de Memoria, Salto y E/S.
• Entonces dado un programa fuente, se traduce cada una de sus sentencias a instrucciones básicas Simplesem en la
memoria para código.
• Cada constructor del lenguaje del programa fuente (invocacioines, bucles, condiciones, Etc.) será traducido a
una secuencia de instrucciones Simplesem que modelen la semántica del constructor.

-----------------------
Esto no esta en filmina... ← Y no se si viene a colación, por que no sé con qué engancharlo en esta parte.
Su estructura difiere en caso de trabajar con LP que soporten manejo estático o dinámico de la memoria. Puede ser estático,
basado en pila o dinámico. Las modificaciones en la estructura, se ven en el área de datos, la cual crece o decrece dependiendo
de los requerimientos.
• Estático: el espacio ocupado por los bloques de código y los datos, es fijo. Se sabe, a priori, cuanto espacio van a
ocupar todos los bloques cargados en memoria. Se cargan todos juntos a pesar de que se permita la ejecución de una a
la vez y no se mueven hasta que termina de ejecutarse todo el programa. El área de datos está compuesta por:
o Puntero a la siguiente instrucción a ejecutar en el módulo invocador.
o Un área común donde se alojan las variables globales.
o Variables locales.
o Resultado de la función.
Ideal para bloques disjuntos.
• Basado en Pila6: el espacio ocupado por los bloques de código y los datos, es predecible. Se calcula viendo la
secuencia de llamado de las unidades. Permite recursividad. Las unidades se cargan cuando se las llaman, con todo su
contenido, sin importar si se utiliza la totalidad del bloque.
o Un puntero al inicio del RA (current)
o Un puntero al final del RA que indica el comienzo de la memoria disponible (free)
o Puntero a la siguiente instrucción a ejecutar en el módulo invocador.
o El link dinámico (LD): tiene la dirección de donde comienza a formarse el área de datos de la unidad
invocadora de la unidad actual.
o El link estático (LE): tiene la dirección de donde comienza a formarse el área de datos de la unidad
contenedora de la unidad actual.
o Un área común donde se alojan las variables globales.
o Variables locales
o Resultados de una función
Ideal para bloques anidados.
• Dinámico: el espacio ocupado por los bloques de código y datos es impredecible. Usan la memoria de manera
desordenada. Se cargan los datos solo cuando se los necesita y se los descarga cuando se dejan de usar.
o Los datos se alojan en una zona de memoria llamada heap.
----------------------

6 Nota: En los LP basado en pila, depende de si son dinámicos o estáticos para saber a que puntero le dan prioridad en caso de necesitar recorrer el ámbito
no local de la unidad.

5
Implementación de tipos de datos en memoria: objetos de datos.

Implementación es la forma en que se representan internamente los tipos de datos con los que trabajan los LP. Los programas
en realidad trabajan con objetos de datos, que son las implementaciones que permiten a los programadores la utilización de los
tipos de datos. Pueden existir distintas implementaciones de un tipo de dato, y un tipo de dato tiene una representación de
almacenamiento y de las operaciones.
• La representación de almacenamiento es la manera en que el objeto de datos es representado en memoria de la
computadora durante la ejecución.
• La representación de las operaciones son los algoritmos o procedimientos para manipular la representación de
almacenamiento elegida.

Representación de datos simples y estructurados. Representación de estructuras estáticas y dinámicas.

Representación de datos simples.


• La representación de almacenamiento de los tipos de datos simples (como los enteros, booleanos, caracteres, Etc.)
está influenciado (o depende) del HW subyacente de la computadora, es decir depende de las capacidades del HW. Se
describe por lo general por: el tamaño del bloque de memoria, la disposición de los atributos y valores de datos dentro
de este bloque. Es muy importante debido a que ciertos tipos se pueden manipular directamente con el HW, si es que
provee las herramientas necesarias y otros deben simularse mediante SW.
• La representación de los atributos, puede ser:
◦ Buscando una mayor eficiencia (de ejecución y de almacenamiento), donde el compilador determina los atributos
del dato (Ver ventajas de ligadura estática)
◦ O puede ser una representación que busque mayor flexibilidad, en este caso los atributos se guardan como parte
del objeto de datos, en tiempo de ejecución a través de un descriptor. (Ver ventajas de ligadura dinámica)
• La representación de las operaciones , se puede hacer directamente como operaciones de HW (suma, resta), o si el
objeto de datos no se puede implementar directamente por HW podría ser mediante simulación de SW ya sea como un
subprograma, procedimiento o función que se suministra como biblioteca del lenguaje, o sino como una secuencia de
código en línea, que a diferencia de la anterior en el lugar donde se llamaría al subprograma se copia el código de la
operación (es decir, las operaciones del subprograma se copian en el punto de invocación).

Algunos ejemplos de representaciones de datos simples son:

• Enteros: utilizan una representación de almacenamiento definida por el hardware. Se representan mediante una
cadena de bits (sistema binario) mas un bit de signo a la izquierda. (Está soportado directamente por hardware). Hay
LPs que permiten utilizar distintas especificaciones o cantidad de bits para representar los enteros. Por ejemplo: en C,
int (32 bits), long (64), short (16)7. Las operaciones con enteros pueden ser aritméticas o relaciónales o de asignación.
• Aritméticas, pueden ser unarias o binarias.
◦ Unaria: int → int.
◦ Binaria: int x int → int.
• Relacionales.
◦ Binaria: int x int → boolean.
• Asignación.
◦ Binaria: int x int → int.

7 Notese que el rango de valores va a depender tanto de la cantidad de bits que se usen, como si se toman enteros con signo (positivos y negativos, más el
cero) o sin signo (solo positivos, más el cero). Por ejemplo, un entero de 16 bits (calificado usualmente como "short"):

6
• Reales (o punto flotante): También se implementan directamente por HW. Su valor se representa por 3 partes,
que son el signo a la Izq, luego el exponente y la mantisa. El rango de valores que puede adoptar y la precisión en
la especificación viene dada por la cantidad de bits que se utilizan para representar los valores. También se
permiten utilizar varias especificaciones. Por Ej..: en C float (32 bits) y double (64). Operaciones: las mismas que
para los enteros y con los mismos resultados.

(Una de las limitaciones que posee este tipo es los errores de redondeo y truncamiento que se acarrean con las
operaciones aritméticas.)

• Decimales o punto fijo: pueden implementarse utilizando una representación definida por el hardware o puede
simularse por software. El hardware de las computadoras que han sido diseñadas para soportar aplicaciones
comerciales, proporcionan soporte para tipo de dato decimal. Es ideal para representar por ejemplo sumas de dinero
con distintas precisión. Ejemplo: en Cobol: dato pic 9999V99 (4 bytes para la parte entera y 2 para la decimal. El “V”
es el separador. Operaciones: las mismas que para los enteros. La ventaja de este tipo de datos es que se puede
precisar el almacenamiento de los valores decimales, es decir, la cantidad de decimales.

• Números Complejos: Puede representarse como un bloque de dos ubicaciones de almacenamiento que contienen un
par de valores reales. Una para la parte real y otra para la imaginaria. Generalmente se simulan por software.

• Racionales: es el cociente entre dos enteros. Usados para evitar errores de truncamiento. Se representan como pares
de entero de longitud ilimitada, por lo que se usa una representación vinculada.

• Caracteres: generalmente el conjunto posible de valores se toma como una enumeración de algún alfabeto (ASCII,
Unicode). Generalmente se implementa directamente por hardware. Si el SO y el HW tienen tablas distintas se debe
hacer una conversión pero si tienen la misma tabla las implementaciones son representadas directamente por el HW.

7
• Booleanos: Es el mas simple de todos los datos. Su rango de valores tiene solo dos elementos (verdadero y falso).Se
utiliza solo un bit de almacenamiento. Como un solo bit no es direccionable, se un byte entero (usando un solo bit, o
toda la cadena para denotar un solo valor verdadero o falso), así que se puede decir que se desperdicia espacio.

• Enumerados: Conjunto ordenado de valores literales. Sirven para ejemplificar un conjunto de valores en los cuales
importa el orden y estos son distintos unos de otros. El programador, define el nombre de los elementos y el orden que
ocuparán dentro de la enumeración. Por ejemplo: en C: enum AnioCursado {Primero; Segundo; Tercero} Cada valor
de la serie de enumeración está representado en tiempo de ejecución por enteros, en el rango 0, 1, 2,... (Los valores
representados nunca son negativos.) Se utilizan sólo los bits suficientes para el intervalo que se requiere. Se omite el
bit del signo. Tomando el ejemplo: type Clase= (Primero, Segundo, Tercero, Cuarto); La variable clase tiene sólo
cuatro valores posibles, representados en tiempo de ejecución como: 0 = Primero, 1 = Segundo, 2 = Tercero y 3 =
Cuarto. Por lo tanto, sólo se necesitan 2 bits para esta representación (porque, por ejemplo, el 3 se representa con 11,
el dos con 10, el cero con 00 y el uno con 01). Al asignarse un valor entero, las operaciones sobre este tipo de datos se
facilitan. La enumeración proporciona ventajas en la legibilidad y fiabilidad de los programas.

• Subrango o subintervalos: Consiste en definir un subtipo de otro ya predefinido, utilizando un dominio acotado que
pertenece y está incluido al dominio del tipo original. Tienen las mismas operaciones que el tipo original, mejoran la
legibilidad, hacen un uso más eficiente del espacio en memoria y permiten obviar ciertos aspectos de control que se
debería tener con el tipo original (por ejemplo que no supere “x” rango de valor). Por ejemplo: en Pascal: type
LetraMayuscula = ‘A’..’Z’ ; type DiasAgosto 1..31; // Se representa de igual forma que el tipo de dato al cual está
reduciendo el rango solo que al acotar el rango disminuye la cantidad de rango requerida (o sea, acotando la cantidad
de bits que se usan para su representación). Mejoran la verificación de tipos. Por ejemplo, index = 1..100; index solo
puede tomar valores de 1 a 100.

Representación de datos estructurados.

Un tipo de datos estructurado es un dato construido a partir de otros datos mas simples, los cuales pueden ser tipos elementales
o estructurados. (Dentro de los tipos de datos estructurados se encuentran: Arreglos. Registros. Unión. Cadenas de caracteres.
Conjuntos.) La representación de almacenamiento está dada por el tipo de los componentes de la estructura y un descriptor
-que es opcional- para guardar los atributos de la estructura) Existen dos modelos de implementación:

• Representación secuencial: La estructura se guarda en un solo bloque contiguo de almacenamiento.


• Representación vinculada: La estructura se guarda en varios bloques no contiguos de almacenamiento, con punteros
que indican el siguiente elemento.

Algunos ejemplos son:

8
• Arreglos: Al ser una estructura de tamaño fijo y con componentes todos del mismo tipo (es decir, homogéneo) se
simplifica el almacenamiento y el acceso a los componentes. La homogeneidad implica que el tamaño y estructura de
los componentes son los mismos. Tamaño fijo implica que el número y posición de cada componente de un vector son
invariables a lo largo de su tiempo de vida.

• Registros: Los campos de registros se almacenan en locaciones de memoria adyacentes. Como el tamaño de los
campos a veces no es el mismo, el método de acceso usado para arreglos no se usa para archivos. La dirección de
desplazamiento, relativa al comienzo del registro, es asociada a cada campo. Los accesos a todos los campos se
manejan usando este desplazamiento.

• Unión: se calcula, por cada componente, la disposición y el tamaño de cada una de las variantes. Se elige la que
necesita mayor espacio en memoria.

Se implementa utilizando la misma dirección para todas las posibles variantes. El tag o discriminante de una unión
discriminada se almacena con la variante en una estructura de registro. En tiempo de compilación, se determina la
cantidad de almacenamiento que se requiere para los componentes de cada variante, y se asigna almacenamiento en
el registro para la variante más grande posible. Al reservarse el mayor espacio posible que puede asumir, siempre hay
espacio. Las disposiciones de los datos se determinan en la traducción y durante la ejecución no se necesita un
descriptor especial porque se utiliza el campo marca o etiqueta (ya definido). Puede realizarse por asociación de las
etiquetas en la declaración. Ejemplo en Ada:

9
type NODO (MARCA : BOOLEAN) is
record
case MARCA is
when true => CONT : INTEGER;
when false => SUM : FLOAT;
end case;
end record;

Representación de estructuras estáticas y dinámicas.

• Cadena de Caracteres: es un tipo de dato en el que sus valores consisten en una secuencia de caracteres del mismo
tipo base. La representación de las cadenas de caracteres puede ser por longitud fija, con longitud variable pero con un
limite máximo o cadenas de longitud ilimitada
◦ Long. Fija DECLARADA → Packed Array de Pascal
◦ Long. Variable con LIMITE DECLARADO → Se guarda en la cabecera el tamaño actual y el máximo.
◦ Long. Ilimitada
▪ Con Asignación fija → Tamaño de los arreglos fijos enlazados, se indica tamaño en la cabecera.
▪ Con Asignación variable → Arreglo continuo que se redimensiona en cada exceso/cambio.

• Conjuntos: Los conjuntos son almacenados cadenas de bits en memoria. Ejemplo: Se puede representar si un
elemento está presente o no de acuerdo a si hay un cero o un uno. Ej.: un set de valores del tipo [`a´ .. `p´] pueden
utilizar los 16 primeros bits de una palabra en los cuales cada bit seteado en 1 representa un elemento presente y cada
bit en 0 representa un elemento ausente. Usando este esquema: [`a´, `c´, `h´, `o´] podrían ser representados como:
1010000100000010.

• Listas: utilizan una representación vinculada. Cada nodo está formado por el dato, y un puntero al siguiente nodo de
la lista.

• Punteros: Un puntero se representa como una dirección de almacenamiento que contiene otra dirección de
almacenamiento. Se pueden utilizar dos representaciones para valores de punteros:
◦ Direcciones absolutas. Corresponde a la dirección de memoria real del bloque de almacenamiento para el objeto
de dato.
◦ Direcciones relativas. Se puede representar como un desplazamiento respecto a la dirección base de algún bloque
de almacenamiento.8
Los punteros son referenciados como valores simples almacenados en dos o cuatro bytes de celdas de memoria,
dependiendo del espacio de almacenamiento de la máquina. Por ejemplo, la operación de asignación: j := ptr ^ , se
implementaría:

8 Como analogía para entender: la ruta absoluta indica la dirección completa del archivo sin importar donde estemos, es decir, si yo tengo una foto de mi
perro llamada miperro.jpg dentro de la carpeta imágenes en mi dominio comocrearunsitioweb.com, la ruta absoluta sería
http://www.comocrearunsitioweb.com/imagenes/miperro.jpg Podemos utilizar esa ruta para llamar a la imagen desde cualquier sitio (ya se a en nuestro
dominio o en otro). La ruta relativa llama al mismo archivo pero DESDE el lugar donde estamos parados, es decir que solo funciona dentro de nuestro
dominio. Por ejemplo: si nosotros estamos en el directorio principal y queremos insertar la foto desde el index la ruta relativa DESDE el index sería:
imagenes/miperro.jpg

10
• Archivos: la implementación depende del SO. Las operaciones se implementan por medio de llamadas a primitivas
del SO. Un archivo es una estructura de datos lineal compuesta por unidades de acceso denominadas registros y un
registro es una estructura de datos formada por uno o mas elementos denominados, campos, que pueden ser diferentes
tipos y que, a su vez, pueden estar compuestos por subcampos. La lectura y escritura se se hace a través de bloques de
registros. (Ver U3)

(Un archivo tiene un nombre lógico o interno que le da el usuario, y un nombre físico o externo que le da el SO.)

Implementación de instrucciones y unidades: asignación, transferencia de control, condición, iteración.

Implementación de instrucciones.
La mayoría de los lenguajes utilizan notación infija para escribir expresiones y esto genera cierta dificultad a la hora de
decodificarlas; por eso se las traduce a una forma ejecutable que permita facilitar la decodificación. Esto se puede hacer de
diversas maneras:
• Mediante secuencia de código, es decir, traducir las expresiones a código máquina, donde el orden de la instrucción
refleja la secuencia de control y al ser interpretadas por el hardware la ejecución es mas rápida.
• Mediante estructuras de árbol, es decir las expresiones se ejecutan siguiendo una estructura de árbol; ésta ejecución
es realizada mediante un intérprete de software que va recorriendo la estructura del árbol.
• Y otra alternativa es traducir las expresiones a la forma prefija o postfija, que si bien se necesitan ciertos algoritmos
que recorran la expresión (de Izq. a Der.), son más fáciles de ejecutar. (La notación postfija es mas directa; la notación
prefija requiere que cada operación invoque recursivamente al intérprete).

Con respecto a la implementación de los distintos tipos de instrucciones están por Ej,: (Completar con Unidad-4)

• La asignación: La sintaxis de la operación de asignación es: <identificador><operador_asignación><expresión>


Se evalúa la expresión (y se traduce a instrucciones de un procesador abstracto) y el valor calculador se almacena en
la dirección en donde se encuentra ligado en memoria el identificador, es decir, el valor calculado se almacena en la
locación de memoria ligada al identificador. Los valores temporarios se mantienen memoria.
• Selección: estas instrucciones proporcionan los medios para elegir entre dos o más caminos en la ejecución de un
programa. Pueden ser unidireccionales, bidireccionales o múltiples. ← CUIDADO CON CONTRADECIRSE
RESPECTO A LO QUE ESTÁ EN LA U4
◦ Unidireccionales: Si-Finsi.
◦ Bidireccionales: Si-Sino-Finsi.
◦ Múltiples o selectores múltiples: case, switch, if anidado.
Estas instrucciones de selección se implementan usando las instrucciones de salto y bifurcar manejadas por el HW.
• Iteración: (Ver Unidad-4) Las instrucciones de iteración también se implementan usando la instrucción saltar (GO
TO) manejada por hardware. En el caso de las iteraciones por contador, las expresiones que definen el valor inicial y
el incremento, se deben evaluar y calcular antes del inicio de la iteración y guardarse en áreas especiales de
almacenamiento temporal.
◦ (Iteración controlada por contador: se implementan con instrucciones del hardware usando la instrucción bifurcar
y saltar. Se debe considerar la salvedad de que si la iteración es controlada por una variable bucle, el valor final y
el incremento se deben evaluar y, luego, almacenar en memorias temporales.)
◦ Si son iteraciones lógicas, la condición debe evaluarse por cada iteración del bloque.
◦ (Iteración controlada por condición: Las diferencias estriban en que la condición se debe evaluar “n+1” veces
para cortar el bucle y en que no requiere de almacenamiento temporal.)

Bifurcación incondicional: (o Transferencia de control incondicional -creo-)se implementa directamente mediante la


instrucción saltar (GO TO) proporcionada por el hardware. (Transferencia de Control incondicional: Se implementa
directamente con la instrucción de salto de HW. Por esta razón es que es muy eficiente.)

11
-------------------------------------
Acá la hice corta, como resumiendo esto de arriba, por que puede ser largo al pedo (o medio confuso):
• ASIGNACIÓN: Se evalúa la expresión derecha, y luego se guarda el valor calculado en la localidad de memoria
asociada al identificador de la izquierda.
• SELECCIÓN (simple, múltiple)...
• ITERACIÓN ...Se implementan con la instrucción
◦ POR CONTADOR (evalúa e incrementa)... SALTAR de HW (que es como un GOTO)
◦ LOGICAS (evalúa siempre)... -se podría asociar SimpleSem acá-
• BIFURCACIÓN INCONDICIONAL...
---------

Implementación de unidades. (Completar con Unidad-5)


La implementación de las unidades se basa en la semántica de las operaciones de enlace, es decir, en el llamado y retorno a una
unidad.
• La llamada a una unidad incluye: métodos de pasaje de parámetros, asignación de memoria para variables locales,
guardar el estado de ejecución de las llamadas a subprogramas, la transferencia de control y la coordinación para el
retorno, y garantizar el acceso a variables no locales.
• El retorno incluye: los valores de retorno en el caso de que el pasaje de parámetros sea out-mode o inout-mode, la
desalocación de variables locales, restaurar el estado de ejecución, y devolver el control a la unidad llamadora.

Una unidad en ejecución, esta representada por un segmento de código; y un registro de activación que contiene toda la
información necesaria para que la unidad pueda ser ejecutada.

Gestión de memoria.
Al activarse (ejecutarse) una unidad, se le asigna un espacio físico de memoria que contiene:
• El segmento de código, formado por las instrucciones máquina a ejecutar.
• Una memoria estática, donde se almacenan constantes y variables locales cuyo valor se mantiene entre llamadas, y
objetos de datos del SO como el ip del proceso, estado de ejecución, Etc..
• La pila, que mantiene las instancias de los registros de activación de los procedimiento que han sido llamados. (La
pila crece cada vez que se hace una llamada a un procedimiento y decrece al momento del retorno)
• Y el Heap, que guarda los datos cuyo tamaño varía en tiempo de ejecución o que no se pueden mantener en la
memoria estática ni en la pila. (El espacio puede ser asignado y desasignado en cualquier momento).

El área intermedia entre la pila y el heap permite a la pila y el heap crecer y usar mas espacio del asignado.

Registro de activación. Esquema de alocación de memoria: estática, basada en pila y dinámica (heap) 9.

Un RA es una estructura de datos de bloques, que contiene la información necesaria para la ejecución de la unidad y son
creados cada vez que se invoca una unidad. La estructura del RA es estática y depende del tipo de lenguaje, es decir, de
acuerdo a la gestión de memoria del lenguaje. Los lenguajes pueden clasificarse, de acuerdo a las decisiones que se toman en el
diseño en relación a la gestión de memoria, en:

Lenguajes Estáticos:
• La alocación de memoria se calcula estáticamente. Es decir, en tiempo de compilación entonces, el espacio requerido
se conoce. (El espacio requerido se conoce en tiempo de compilación)
• Cada segmento de código queda ligado a una única instancia de RA, por lo que cantidad de RA es fija (uno por cada
segmento de código). Es por esto que no permiten recursividad (o sea, no permiten ligar un mismo segmento a varios
RA)

9 En este tema, uno se puede re mambear con ejemplos y terminología y autores que ponen las cosas similares pero distintas. La versión de Nico tenía
planteados los ejemplos y todo. PC.

12
• Son lenguajes sensibles a la historia (mantienen los datos): dos ejecuciones con las mismas entradas para variables
globales y parámetros pueden producir resultados diferentes. El tiempo de vida de las variables es el tiempo de
ejecución de la unidad.
• Ventajas: Esquema sencillo de implementar. Simple.
• Desventajas: No permite modelar recursividad; mantiene ocupada la memoria con la totalidad de las unidades en todo
momento, aún cuando las unidades no están activas. Las carga al inicio y las descarga una vez que finaliza el
programa.
• En este tipo de esquema el registro de activación está formado por la dirección de retorno, los parámetros, las
variables locales y el valor de retorno (o Valor de función) en caso que sea una función.

Lenguajes basados en pilas:


• La cantidad de memoria requerida no puede calcularse estáticamente. Sino que se calcula cuando alcanza la ejecución
de la unidad. Cuando una unidad se activa, se crea una instancia de un RA. (Esto incluye actualización de punteros.)
Lo que implica la alocación en memoria para mantener el ambiente de referenciamiento local de la unidad activada y
los datos manejados por el sistema. (Es decir, la memoria estática, implica asignar memoria a las variables y a la
informacion necesaria para el SO de los procesos)
• Si empieza una nueva ejecución de la misma unidad, antes de que termine, se crea otra instancia del RA totalmente
independiente de la anterior (es decir, que SI permite recursividad -en el estatico si empieza otra ejecucion antes que
termine la primera, se usa el mismo RA-) y con un nuevo ambiente de referenciamiento local, entonces NO es
sensible a la historia.
• Cuando se termina de ejecutar una unidad, se destruye la instancia del RA correspondiente y se libera espacio en
memoria.
• En este tipo de lenguajes el RA está formado por la dirección de retorno, el enlace dinámico, los parámetros, las
variables locales y el valor de función.

13
Lenguajes dinámicos:
• Hacen un uso impredecible de la memoria. (Ya que al permitir un tamaño dinámico del RA no se puede realizar la
reserva de memoria exacta cuando la unidad se carga en memoria).
• Se utiliza una estructura del tipo heap que es un área de la memoria que mantiene estos datos dinamicamente. (Es
decir, el área de memoria utilizada se llama heap).
• La estructura del RA es la misma que la basada en pila.
• (Se reserva, en compilación, espacio para lo que se sabe que se va a usar y que se puede determinar cuanto espacio
ocupa y el resto si en ejecución. Por ejemplo: el descriptor de un vector dinámico: que es un puntero al mismo con
algunos campos extras para su correcta manipulación (cantidad de dimensiones y límites por ejemplo)).

-------------

14
PREGUNTA A CARABIO: ¿Podría decirnos la/las diferencia/s conceptuales de los lenguajes basados en pila y los
dinámicos? (O sea, las diferencias entre ambos esquemas)

RESPUESTA: La diferencia más importante es que en los lenguajes basados en pila el cálculo de memoria necesaria para
alocar las variables utilizadas se realiza en el momento en que la unidad alcanza la ejecución, mientras que en los lenguajes
dinámicos se realiza a medida que se necesita durante la ejecución. Es decir no existe una reserva de memoria previa, sino que
se va alocando memoria arbitrariamente. Generalmente esto se realiza utilizando punteros.
-------

Implementación de ambiente de referenciamiento: reglas de alcance: estático y dinámico.

Recordar Alcance y visibilidad: El alcance es la porción de código del programa en donde una entidad es visible, y una
entidad es visible en una sentencia, si puede ser referenciada en esa sentencia. (Ampliar con U3)

Ambiente de referenciamiento: Es el conjunto de entidades (ligadas) que son visibles o pueden ser referenciadas en una parte
determinada del código. Dicho ambiente se crea cuando se empieza a ejecutar la unidad y permanece invariante durante todo
su tiempo de vida. Lo que puede variar, son los valores contenidos en los objetos de datos. Está compuesto por:
• El Ambiente local: (representado por el conjunto de ligaduras establecidas al momento en que se ingresa a un
determinado subprograma.) Es el conjunto de todas las entidades creadas dentro del subprograma actual.
(Conformados por los parámetros formales y subprogramas definidos dentro del subprograma en cuestión.)
• Ambiente global: asociaciones creadas al inicio de la ejecución del programa o bloque principal. Es decir, son
entidades globales que son visibles en todo el programa. (Forman parte del ambiente no local.)

Reglas de alcance: Las reglas de alcance determinan como se asocian las referencias a variables no locales con sus
correspondientes declaraciones y atributos.
• Alcance estático: hace referencia al conjunto de entidades que podemos referenciar siguiendo al enlace estático de los
registros de activación de las unidades que forman parte de los antepasados estáticos de la unidades cuestión. Los
antepasados estáticos son las unidades que contienen a la unidad en cuestión en el código fuente. Las ligaduras
estáticas son guardadas en la posición “dos” (llamada offset) del RA, para cualquier RA (la “cero” es para el puntero
de retorno y la “uno” es para la dinámica). (Las referencias se resuelven en tiempo de compilación. Es decir, que
cuando el compilador encuentra una referencia a una entidad, los atributos se determinan buscando una declaración de
dicha entidad siguiendo la cadena sus antepasados estáticos. Los programas con alcance estático son más fáciles de
leer, más confiables y se ejecutan más rápido que los programas con alcance dinámico.)
• Alcance dinámico: hace referencia al conjunto de entidades que podemos referenciar siguiendo al enlace dinámico del
registro de activación de las unidades que forman parte de los antepasados dinámicos de la unidades cuestión. Los
antepasados dinámicos son las unidades que fueron llamando a la unidad en cuestión en ejecución. Las ligaduras
dinámicas son guardadas en la posición “uno” del RA (la “cero” es para el puntero de retorno). Siguen el orden
inverso a la cadena de llamadas para buscar las variables no locales. (Las referencias se resuelven en tiempo de
ejecución siguiendo el flujo de activación de las unidades)

Cadena estática. Cadena dinámica. Acceso al entorno global y local. 10

Para los LP Estáticos, las referencias no locales se pueden resolver con:

Cadena Estática:
Se llama así al conjunto de RA a los que se pueden hacer referencia siguiendo el puntero que denota el enlace estático. Busca
en el contenido que se encuentra dentro de la 2da posición u offset, en la cual figura la dirección donde se encuentra el RA del
padre estático. (La cadena estática es una cadena de enlaces estáticos que vincula instancias del registro de activación en la
pila. La cadena estática de una instancia de RA conecta a todos sus antepasados estáticos. Para esto se tiene en cuenta también
la profunidad estática que es un numero que determina el nivel de profundidad de cada unidad. Entonces, una referencia a una
variable no local puede ser representada por el par: desplazamiento de cadena (= a la profundidas estatica donde esta
referenciada, menos, la profundida estatica de la unidad donde esta declarada), desplazamiento local en la pila (es decir, la
posicion en el RA).

10 Ccreo que el tiulo más correcto es "Implementaciuón de referencias no locales". Así está en el resumen final de la filmina.

15
Display: consiste en colocar los enlaces estáticos (serían los punteros -EE-) en una pila llamada display, donde las entradas en
la pila son punteros a las instancias de registro de activación que tienen las variables en el ambiente de referenciamiento. En
este caso una referencia se representa por: desplazamiento en el display, desplazamiento local. Donde depplazamiento en el
display es lo mismo que desplazamiento de cadena.

En los lenguajes con alcance dinámico también existen dos técnicas para determinar las referencias a variables no locales:

Acceso profundo: Las referencias de las variables no locales se encuentran buscando en las instancias de RA en la cadena
dinámica, es decir, siguiendo el flujo de activación de las unidades. A diferencia con la cadena estática, la longitud de la cadena
no puede determinarse estáticamente y cada instancia de RA debe tener los nombres de las variables.

Acceso superficial: Se diferencia con el acceso profundo en que las variables no están almacenadas en las instancias de RA.
Puede haber una pila para cada nombre de variable (muy costoso) o una tabla central con una entrada para cada nombre de
variable (acceso mas rápido).

-----
Acceso al entorno local y global:
• Acceso al entorno global: las variables globales son direccionadas a través de direcciones absolutas.
• Acceso al entorno no local y local: existe un argumento que calcula la distancia relativa de la variable al offset de
dicho RA.

16
===============================
===============================
Se puede hacer un resumen general de todo esto (desde "tipos de lenguaje según su manejo de memoria").

Hay 3 tipos de LP:

• Estaticos: todo el código y los respectivos IRAs para cada unidad están cargados en memoria.
◦ El RA está compuesto por DIR_RETORNO – PARAMETROS – VAR_LOCALES – VAL-FUNCION.
◦ No permite recursividad.
◦ Siempre usa el espacio total de almacenamiento que le corresponde, aún y cuando una unidad no esté activa.
◦ Este tamaño se calcula en tiempo de compilación en base al tamaño y cantidad (fija) de unidades.
◦ Es un modelo simple simple.
• Basados en pila: la IRA de una unidad se crea recién cuando esta se activa; y finalizada la ejecución de la misma, se
la destruye.
◦ El RA está compuesto por DIR_RETORNO – ENLACE_DINAMICO (a la unidad llamadora) - PARAMETROS
– VAR_LOCALES – VAL-FUNCION.
◦ Permite recursividad.
• Dinamicos (heap): hay un uso impredecible de memoria, por lo que el tamaño del RA se conoce a medida que se va
ejecutando la unidad. Sin embargo, se puede reservar espacio para lo que se "conoce" (como ser descriptores,
punteros, enlaces a la unidad llamadora, Etc.) al momento de iniciar la unidad, y luego ir extendiendo y
comprimiendo el tamaño de la IRA (usando el área de heap).
◦ El RA es el mismo que el los basados en pila.

Para hablar de Implementación de Ambiente de Referenciamiento, hay que recordar 3 conceptos:


• Alcance: proción de programa dentro de la cual una entidad es visible (una entidad ES visible dentro de su alcance).
• Ambiente de referenciamiento: conjunto de entidad ligadas que pueden ser referenciadas (o sea, que son visibles) en
una sentencia.
• Reglas de alcance: determinan como una ocurrencia de un nombre es asociada con una variable (especialmente
cuando se trata de referencias no-locales). Tienen una mayor importancia en LP con estructura de bloques anidados.

En base a esto, se hace hincapié en la resolución de referencias no-locales; según las reglas de alcance que se planteen en el LP,
entonces, hay dos tipos:

• LPs con ALCANCE ESTATICO: las referencias se resuelven en tiempo de compilación. Se considera, dada una
referencia a una variable no-local, esta se encuentra en su(s) padre(s) estáticos(s). Todas las variables no-locales
residen en algún IRA (las reglas y controles semánticos seguran esto, de lo contrario habría una referencia a una
variable no declarada). Para resolver estas referencias, se pueden emplear dos métodos:
◦ CADENA ESTÁTICA: se sigue la cadena de padres estáticos (usando los enlaces estáticos de los IRA
correspondientes) hasta encontrar la referencia.
◦ DISPLAY: se colocan los enlaces estáticos en una pila (display), donde las entradas en la pila son punteros a las
IRA que tienen variables en el ambiente de referencia.
• LPs con ALCANCE DINÁMICO: las referencias se resulven en tiempo de ejecución, siguiendo a la inversa la
cadena de llamadas de los subprogramas (usando los enlaces dinámicos de los IRA correspondientes). Esto se puede
hacer de dos maneras:
◦ ACCESO PROFUNDO: se siguen, literalmente, los enalces dinámicos hasta encontrar el RA que contenga la
referencia solicitada.
◦ ACCESO SUPERFICIAL: las variables no están almacenadas en las IRAs, sino en una tabla central que tiene
una entrada (columna) por cada variable, y luego se van apilando en cada una los subprogramas que las definen
(arriba está el dibujito).

=============================
=============================

17

También podría gustarte