Contenidos
1. Conceptos Básicos 2. Datos
- Datos Simples - Datos Estructurados 3. Operaciones
- Operaciones Primitivas - Operaciones Extensibles
CONCEPTOS BÁSICOS
Aspectos de un Lenguaje de Programación (LP) - Datos: estructuras para representar información
- Operaciones primitivas: estructuras para manipular datos - Control de instrucciones: control de secuencia
- Control de Datos: referenciamiento - Almacenamiento: control de alocamiento
- Entorno de Operación: comunicación con el exterior
Los diferentes paradigmas de programación se diferencian por la forma en que aplican estos conceptos.
Binding (enlace, ligadura o atadura)
Asociación entre un atributo y una entidad o entre una operación y un símbolo (a = 4 o a es un procedimiento)
Tiempo de Binding: momento en que tiene lugar el binding.
- Al momento de definir/diseñar el LP: la estructura del LP (tipos, control, operaciones)
- Al momento de implementar el LP: representación de datos (hardware), manejo de memoria, implementación de operaciones.
- Al momento de escribir un programa: (binding estático en tiempo de compilación) Tipos para variables, estructuras de datos, etc.
- Al momento de ejecutar un programa (binding dinámico) variables a sus valores, lugares de memoria a referencias, parámetros formales a parámetros actuales.
Ejemplo
int count; -
-
count = count + 5;
- Conjunto posible de tipos para count: binding al momento de definir el LP. - Tipo de count: binding en tiempo de compilación
- Conjunto de posibles valores para count: binding en tiempo de compilación - Valor de count: binding en tiempo de ejecución
- Conjunto de posibles significados para el símbolo +: binding en tiempo de compilación - Representación interna del literal 5: binding en tiempo de compilación e implementación.
El tiempo de binding es muy importante para entender la semántica de un LP, por ejemplo:
a) para entender que hace un programa o debe hacer para asociar los parámetros actuales en una llamada con los parámetros formales
b) para determinar el valor actual de una variable, es necesario saber cuando la variable fue asociada a su almacenamiento
El tiempo de binding responderá la pregunta ¿cuándo ocurren las cosas, en tiempo de compilación o en tiempo de ejecución?
Cambios pequeños en la definición del LP producen cambios en los tiempos de binding que afectan totalmente las características de un LP.
Cualquier LP se puede considerar como la especificación de un conjunto de operaciones que se van a aplicar a ciertos datos en un determinado orden. Existen diferencias entre los LP en cuanto a los tipos de datos, las operaciones disponibles y los mecanismos que suministran para controlar el orden en que las operaciones se aplican a los datos. Estas tres áreas: datos, operaciones y control forman el marco de análisis y comparación de los LP.
DATOS
Objetos de datos, variables y constantes
a) A:
b) 10001
c) A: Variable enlazada: objeto de datos cuyo valor de datos es 17.
Algunos objetos de datos que existen durante la ejecución de un programa son definidos por el programador: variables, constantes, arreglos, archivos, etc. Otros objetos de datos son definidos por el sistema, son objetos que construye el sistema para “mantenimiento” durante la ejecución del programa y a los cuales el programador no tiene acceso, por ejemplo: pilas de almacenamiento, registro de activación, etc.
Un objeto de datos es un recipiente para valores de datos (lugar donde se puede guardar un valor y luego recuperar). Un objeto se caracteriza por un conjunto de atributos (tipo).
Valor de datos: puede ser un solo número, carácter o posiblemente un apuntador a un objeto de datos. Un valor de datos se representa por medio de un patrón particular de bits.
En el curso de la ejecución de un programa ciertos objetos de datos existen al principio de la ejecución y otros se crean dinámicamente durante la misma. Algunos objetos de datos se destruyen durante la ejecución, otros persisten hasta que el programa concluye. Cada objeto tiene un “tiempo de vida” durante el cual puede usarse.
Los objetos de datos pueden ser elemental (simple) o estructuras de datos.
Un objeto de datos participa de varios enlaces durante el tiempo de vida, mientras que los atributos de un objeto de datos, no varía durante su tiempo de vida. Enlaces más importantes:
1. Tipo: asociación que comúnmente se hace en tiempo de compilación, asociando al objeto de dato con un conjunto de valores que pueden tomar.
2. Localidad: enlace del objeto de datos con una localidad de almacenamiento en la memoria. Por lo general no puede ser modificado por el programador sino que es establecida y modificado por rutinas de gestión de almacenamiento.
3. Valor: enlace por lo general resultado de una asignación. datos control operaciones
Objeto de dato: una localidad en la memoria con el nombre A
Valor del dato: patrón de datos usado por el traductor para el número 17.
4. Nombre: enlace a uno o más nombres por medio de los cuales se puede hacer referencia durante la ejecución del programa. Por lo general se establece mediante declaración.
5. Componente: el enlace de uno o más objetos de datos a uno o más objetos de datos (pointers)
Los objetos de datos variables pueden ser simples o complejos, tienen un nombre y se les da valor mediante una asignación. Las constantes, son simples y tienen nombre.
Tipos de datos
El tipo de un dato indicará los valores que puede tomar y las operaciones que se le pueden aplicar.
Cuando se diseña un LP deberá especificarse para cada tipo de dato: atributos, valores y operaciones. Cuando se implementa el LP deberá implementarse para cada tipo de datos: representación del almacenamiento y representación de las operaciones (como manipulará los objetos de datos).
Declaraciones
Para el LP la declaración es una operación que le permite al compilador organizar el manejo de datos. Para el programador la declaración es un enunciado donde especifica: identificadores y tipo de variables (se hace binding). El lugar de la declaración implica determinar el tiempo de vida.
En Pascal hay una sección para declarar variables var
entero : integer; r : real;
En C, las variables deben declarase antes de su uso: int c;
float f;
El caso de C como Pascal son declaraciones explícitas. En Fortran la variable índice es por omisión integer, ya que toda variable cuyo nombre comience con I-N es entera, caso contrario debe declararse: REAL indice. En la declaración de constantes se especifica el valor y nombre de la misma:
En Pascal: const a = 30; En C:
#define A=30;
Ejemplo: Var
A : array[2..20] of integer;
Permite al compilador establecer: - Donde se reserva memoria - Cuanta memoria se reserva
- Que valores representa y cuántos de ellos - cuando son accesibles los valores
- cuando se destruye la memoria
Toda esta información es en general “estática” y por lo tanto permite que el compilador optimice notablemente su manejo.
Si no existen declaraciones una operación como x + y es “genérica” (se refiere a varios tipos de adición) y por lo tanto para implementarla hay que chequear los tipos de las operaciones en tiempo de ejecución (flexible pero no eficiente).
Representación del almacenamientos de tipos de datos (implementación)
1.Datos Simples
a) Números (enteros y reales) por lo general se utiliza la representación del hardware con descriptor estático (implícito) pero hay LP que utilizan descriptor dinámico (explícito) como Lisp y Prolog.
Enteros: α signo Numero (bits)
Flotantes: α Signo
expon.
Signo Mantisa
Exponente Mantisa
α signo datos
b) Carácter: la representación de almacenamiento es igual que para los enteros solo que no requiere bits para el signo, lo cual lo hace más sencillo (0-127 ASCII)
c) Strings
Longitud de declaración fija:
α R E L A
T I V I
T Y
Longitud variable con límite:
α 10 14 R E
L A T I
V I T Y
Limitada con asignaciones fijas:
α 10 REL → ATIV → ITY
Limitadas con asignaciones variables: α R E L A T I V I T Y
d) Punteros: (referencias – direcciones) direccionamiento indirecto.
α → entero
α → string
Descriptor: cantidad de bytes, signo
α dirección en la tabla de símbolos
Cadenas guardadas a 4 caracteres por palabra (12). Con relleno de espacios en blanco.
Longitud actual y máxima de la cadena (pascal) guardada en la cabecera de la cadena
Cadenas guardadas a 4 caracteres por bloque cabecera. ( C )
2. Datos Estructurados
- Arreglos
a) Homogéneos de tamaño fijo
Referencia LI Límite inferior
Descriptor LS Límite superior
Tipo ... N ...
α
α A[LI]
Memoria A[LI+1]
Alocada y Datos
estática A[LS-1]
A[LS]
Se organiza con un descriptor que contiene los límites del subíndice al tipo base y la dirección de comienzo. El almacenamiento se hace en bytes contiguos.
Para acceder a A[I] con dir = α + ( I – LI ) * N
En Pascal, el tipo del índice puede ser cualquier escalar. Por dicha razón el descriptor del arreglo debe almacenar información referida al tipo del índice y el cómputo de la dirección de acceso se complica un poco.
b) Matrices y arreglos multidimensionales
Aunque las matrices las pensamos como una disposición plana, cúbica, etc. se almacenan fila por fila o columna por columna.
LIF α A[1,1]
LSF A[1,2] 1er. fila
descriptor LIC
LSC A[1,LSF]
Tipo A[2,1]
A[2,2]
2da. fila
... A[2,LSF]
longitud fila = (LSC – LIC + 1) * N = F dir A[i,j] = α + (i – LIF) * F + (j – LIC) * N
optimizado: α + i + F + j * N
Descriptor de tipo
N: cantidad de bytes
.... N... ...
- Registros
El almacenamiento es secuencial pero el descriptor es más complejo. El acceso es en general aleatorio.
nombre 1 tipo_campo1 α1 campo1 R
nombre 2 tipo_campo2 α2 campo2 E
G
I S T R
αn campon O
- Fórmula de acceso al campo i: αi = α1 + Σ j > i long(campoj)
- La sumatoria es necesaria porque los campos tienen diferente tamaño. Pero como el tipo de los campos es conocido en tiempo de compilación se puede determinar los tamaños y optimizar, el método de acceso a:
αi = α1 + Ki (se conoce como dirección base más desplazamiento). - Donde Ki se puede agregar al descriptor.
- Conjuntos
Hay dos formas de almacenamiento: a) Cadenas de bits
Universo de valores del conjunto : e1, e2, ...en. El conjunto S se representa por una cadena de bit como:
αs → 1 0 1 0 0 1 0 0 1
e1 en
- Significa que el elemento e1 está en el conjunto y el elemento e2 no. - Se usa el bit 1 para indicar que el elemento esta y el bit 0 para lo contrario. - Esta implementación sirve para universos pequeños.
b) Dispersión – Hash
0 → →
1 2
N → →
K es el elemento del conjunto.
Para determinar si un elemento x esta en el conjunto se calcula d y luego se recorre la lista.
Las operaciones de inserción y eliminación son eficientes, no así la unión, la intersección y la diferencia.
OPERACIONES
Tipos de operaciones
- Sobre los datos del usuario (las usuales)
- Sobre los datos del sistema (llamadas a subprogramas, declaraciones, pasaje de parámetros, etc.) descriptor
K
Existen multitudes de operaciones, desde la suma hasta la unificación, es imposible e inútil abarcar todas.
El conjunto de operaciones definidas para un tipo de datos determina como se pueden manipular los objetos de datos de ese tipo. Las operaciones pueden ser operaciones primitivas, las operaciones se especifican como parte de la definición del LP o pueden ser operaciones definidas por el programador (subprogramas, métodos).
Una operación es una función matemática (por lo general)
- Para un/unos argumento/s de entrada dado, tiene un resultado definido, único y determinado - Los argumentos (operandos) y el resultado tienen tipos establecidos, dominio y rango.
- Un algoritmo es un método para especificar la acción de la operación (como calcula el resultado), pero se puede utilizar otros métodos de especificación.
Signatura: número, orden y tipos de datos de los argumentos y número, orden y tipo de los resultados. Es conveniente usar la notación matemática:
nombre_operación : tipo_argumento x tipo_argumento x... tipo_argumento → tipo_resultado
Ejemplos
a) adición de enteros: + : entero x entero → entero b) igualdad: = : entero x entero → booleano
c) raíz cuadrada: sqrt : real→ real
- En C la signatura es el prototipo de la función.
- Una operación que tiene dos argumentos y un resultado se denomina operación binaria o diádica. Cuando tiene un resultado y un argumento se denomina operación unaria o monoadica.
La implementación de operaciones es en general “parcial y oscura” (factores que hacen confusa la
definición de la operación)
Operaciones indefinidas
Argumentos implícitos
Efectos colaterales
Automodificación
Operaciones indefinidas: operaciones que no están definidas para ciertas entradas (excepción – underflow) Argumentos implícitos: la operación puede acceder a argumentos implícitos a través de variables globales u otras referencias no locales, lo cual deja sin resolver la cantidad de datos que utiliza la operación.
Efectos colaterales: (resultados implícitos) Una operación puede devolver un resultado explicito, pero también puede modificar los valores guardados en otros objetos de datos, tanto definidos por el programador como por el sistema. Estos resultados implícitos se conocen como efectos colaterales. Una función puede modificar sus argumentos de entrada y también devolver un valor.
Automodificación: Una operación puede modificar su propia estructura interna, ya sean sus datos locales que se retienen entre ejecuciones o su propio código. Los resultados que la operación produce, por lo tanto, dependen no solo de los argumentos de entrada sino del historial completo de llamadas anteriores (operaciones sensibles al historial). Ejemplo: el generador de números random o aleatorios, que aunque se les da los mismos argumentos genera diferentes números. Esto es porque guarda como dato local “el número semilla” que se modifica entre llamada y llamada, y se utiliza para el cómputo.
La implementación de operaciones puede hacerse: - Directamente como operación de hardware
Declaración de operaciones
Por lo general las operaciones primitivas no deben ser declarados (si los argumentos usados en las mismas). Sin embargo, si es común declarar la signatura completa de las operaciones definidas por el programador (funciones o procedimiento)
Ejemplo
float Sub(int X, float Y) // en C
function Sub(X: integer, Y: real) : real // en Pascal
Sub tiene la siguiente signatura: Sub : int x float → float El objetivo es facilitar el chequeo estático de tipos.
Operaciones Primitivas
1. Aritméticas: Los operandos y el resultado son números. Puede haber excepción-overflow pero no hay efectos colaterales, operadores implícitos o automodificación. La diferencia fundamental entre los diferentes LP es si puede hacer chequeo estático.
2. Relacionales: Los operandos son (en gral.) del mismo tipo (números, chars, strings) y el resultado es un booleano (o bien, 0 y 1 en C). En algunos casos la declaración de tipos permite chequeo estático.
3. Booleanas: recibe y entrega boléanos (0-1)
4.Conversión de tipos: si durante la verificación de tipos (estática o dinámica) ocurre una discordancia entre el tipo real de un argumento y el tipo esperado para esa operación, entonces:
- error (compilación o ejecución)
- se aplica una coerción (conversión implícita de tipos) para cambiar el argumento de tipo real al tipo correcto.
Signatura: conversión_operación : tipo1 → tipo2 Casi todos los LP proveen conversión de tipo:
- Explícitas: las hace el programador. Por ejemplo, la función round en Pascal, cast en C y Java. - Implícitas: las hace automáticamente el lenguaje.
Ejemplos:
Si B es entero y C es real
A: = B + C; // Tanto Pascal como C pasarán B a real antes de hacer la adición Si P es longint y Q es integer
L := P + Q; // se pasa Q a longint
El principio que gobierna las coerciones es “no perder información” :
Ensanchamiento/promocion: shortint → longint
integer → real
Estrechamiento: real → integer (pérdida de información)
- Las coerciones de estrechamiento por lo general son explícitas.
- Las coerciones implícitas implican agregar código (lo hace el compilador).
- En Pascal y Ada se hacen muy pocos coerciones implícitas por lo general la discordancia de tipos de error, en cambio C el compilador tratará de suministrar un cambio de tipo (son la regla).
5. Asignación: es una operación. Su resultado es implícito (modifica el estado de una variable) El resultado puede ser explícito (C) o puede no estar permitido (Pascal).
La asignación es una operación básica para combinar el enlace de un valor de un objeto de datos. Trabaja por efecto colateral. Recibe un operando y modifica el contenido de la referencia.
Supongamos que E se evalúa sin problemas pero es de tipo diferente a R. Si el tipo puede convertirse sin colisión (entero a real) entonces se hace, sino:
- error en tiempo de ejecución (Pascal). - conversión automática del resultado (C) . - conversión automática del tipo de R. (Lisp).
Semántica de la asignación: A = B o A := B (A y B son enteros)
A = B o A := B (A y B son punteros)
Significa asignar una copia del valor de la variable B a la variable A.
Significa hacer que A se refiera a lo mismo que se refiere B.
Otras cuestiones semánticas: A = 2 + 3
a) significa evaluar 2+3 y asignar su valor 5 a A? b) Significa asignar la operación “2+3” a A?
- En los lenguajes con tipos estáticos de datos, el tipo para A decide cual semántica se va a seguir: si A es de tipo entero entonces a A se le asigna el valor 5; si A es de tipo operación, la segunda semántica es la adecuada.
- En los lenguajes con tipos dinámicos, donde a A se le confiere un tipo en virtud de la asignación, ambas semánticas son aplicables.(Prolog)
Operaciones extensibles (subprogramas)
Funciones: retornan un valor explícito y se usan en expresiones.
Procedimientos: Subrutinas que se invocan con llamadas y si devuelven resultados es por efecto colateral.
- En Pascal y Modula-2 están ambos tipos de subprogramas, pero no se usa CALL sino llamada implícita. - En principio las funciones no siempre pueden ponerse en asignaciones: for i:= f(x)....
- En C solo hay funciones, lo cual hace más homogéneo el tratamiento en general.
- Las operaciones en un subprograma son los parámetros (formales o actuales) En algunos lenguajes, el espacio de memoria para dichos parámetros y los resultados, deben reservarse “antes de la ejecución” (Pascal). Además el tipo de resultado está limitado a los predefinidos. (se solucionan con estructuras dinámicas y pasajes por referencia).
Un subprograma es una operación abstracta definida por el programador. Casi todo los LP ofrecen facilidades para definir e invocar subprogramas. Debemos tener en cuenta dos puntos de vista:
1. Diseño de programas: el sentido que tiene el subprograma para el programador que lo define (varias operaciones primitivas)
2. Diseño del LP: aspectos del LP que posibilitan al programador definir subprogramas (tipo de compilador) y su implementación que posibilita la invocación de los subprogramas en tiempo de ejecución.
Definición de un subprograma: 1. Especificación
2. Implementación
- Signatura
- Acción (descripción de la función del subprograma)
Ejemplo:
float FN (float X, int Y) // C
function FN (X: real; Y: integer) : real; // Pascal
- signatura: FN : real x entero → real - argumentos: los parámetros
Cuando un subprograma devuelve explícitamente un resultado se denomina por lo general “función”. Pero algunos subprogramas devuelven más de un resultado o ninguno. Se denominan procedimientos o subrutinas.
Ejemplo
void Sub (flota X, int Y, flota Z, int *W); // en C
procedure Sub (X: real, Y: real, Var Z: real, Var W: real); // en Pascal
Estos subprogramas, pueden modificar los valores de los argumentos (E/S) devuelven resultados implícitamente.
- signatura: Sub : real1 x entero1 → real2 x entero2
¿qué significa la siguiente signatura? Sub : real1 x entero1 → real1
Problemas en la descripción precisa de la función de subprogramas:
1. Un subprograma puede tener argumentos implícitos (variables globales)
2. Un subprograma puede tener resultados implícitos (efectos colaterales: cambios a variables no locales, argumentos de e/s)
3. Un subprograma puede no estar definido para ciertos argumentos posibles, de modo que no completará su ejecución, transfiriendo el control a algún manejador de excepciones externo y termina abruptamente la ejecución del programa completo.
4. Un subprograma puede ser sensible al historial, de modo que sus resultados dependen de los argumentos dados a lo largo de todo el historial pasado y no solo de los argumentos dados en una sola llamada (por ejemplo si se retienen datos locales entre invocaciones)
Implementación de un subprograma:
Un subprograma se implementa usando estructuras de datos y operaciones que suministra el LP. La implementación por lo general está definida por un cuerpo: declaración + enunciados.
float FN(float X, int Y) // signatura {
float m; // declaraciones locales int n;
-
- // enunciados -
}
En algunos lenguajes (Pascal) el cuerpo puede incluir más subprogramas.
Verificación de tipos:
Se hace igual que para las operaciones primitivas (sobre argumentos y resultados)
El LP puede proveer coerciones de tipos para los argumentos.
La diferencia está en si el programador puede o no declarar explícitamente esta información.
Definición e invocación de subprogramas
Los diferentes LP se diferencian bastante en la forma que manejan no solo la definición de subprogramas sino principalmente la invocación, este es un problema fundamental, quizás el más importante.
El programador escribe una “definición” de subprograma como una propiedad estática (tiempo de compilación); durante la ejecución si se llama (invoca) el subprograma se crea una “activación” del subprograma. Cuando se completa la ejecución del subprograma la activación se destruye. Si se hace otra llamada, se crea otra activación. A partir de una sola definición se pueden crear muchas activaciones durante la ejecución del programa. Es decir definición ≠ activación, la información disponible es diferente.
Pero la definición se considera una “plantilla” para crear activaciones. Cuando un subprograma se activa deberán estar disponibles todos los datos que requiera (argumentos, locales) es decir, una activación se representa como un bloque de almacenamiento que contienen ciertos elementos de datos componentes que son importantes para la activación del subprograma. Debe asignarse almacenamiento cuando se destruye una activación. Tienen un “tiempo de vida”, el tiempo durante la ejecución entre la “llamada” que la crea y el “retorno” que la destruye.
Implementación de definición e invocación de subprogramas
function FN (X: real; Y: integer) : real; const
valinic = 20; valfinal = 10; var
N : integer;
M : array [1..10] of real; begín
- -
N := valinic – Y;
if N < valfinal then ... -
-
FN := 120 * X + M[N]; end;
Esta definición especifica los componentes necesarios para una activación en tiempo de ejecución:
1. El renglón de la signatura suministra información para el almacenamiento de parámetros (X e Y) y el almacenamiento del resultado, de tipo real.
2. Las declaraciones indicarán la necesidad de almacenamiento de variables locales (M y N).
3. Almacenamiento de literales y constantes valinic = 20 y valfinal = 10 como constantes, 20 y 10 como literales.
4. Almacenamiento para código ejecutable.
Por lo general la plantilla se divide en dos:
Prólogo para crear Punto de retorno
registro de activación y otros datos del sistema
Código ejecutable para cada Datos de resultado de FN
enunciado de subprograma X : parámetro
Epílogo para borrar Y : parámetro
registros de activación M: objeto local de datos
10 -
20 N: objeto local de datos
2 -
Segmento de código para el subprograma FN Registro de activación para FN (plantilla)
Parte estática: segmento de código (constantes, código, literales). Esta parte no varía durante la ejecución de subprogramas y por lo tanto una solo copia puede ser compartida por todas las activaciones.
Parte dinámica: registro de activación (parámetros, variables locales, resultados, otros datos de almacenamiento). Esta parte tiene la misma estructura para cada activación, pero diferentes valores. Cada activación tienen una copia propia del registro de activación.
Almacenamiento Estático Almacenamiento Dinámico
Registro de Activación
para FN Invocaciones
sucesivas Segmento de Código
para FN
Registro de Activación para FN
del subprograma FN
Registro de Activación para FN
Solo se almacena un segmento de código para toda la ejecución de todo un programa (creado estáticamente).
Los registros de activación se crean y destruyen dinámicamente durante la ejecución. Cada vez que se llama al subprograma y cada vez que el mismo concluye.
El tamaño y estructura del registro de activación se puede determinar durante la traducción. Por lo tanto el compilador puede determinar cuántos componentes se necesitan para guardar los datos necesarios en el registro de activación y la posición de cada componente en el mismo. Se puede tener acceso a los componentes usando el cálculo de dirección base más desplazamiento para registros ordinarios.
Para crear un nuevo registro de activación solo se requiere que se conozca el tamaño del bloque de almacenamiento del registro, en vez de su estructura interna en detalle. En lugar de guardar una plantilla completa de registro de activación en tiempo de ejecución solo se debe guardar el tamaño del registro de activación para que lo use la operación de “llamar” al crear el registro de activación. La gestión de almacenamiento en la llamada y retorno de subprogramas implica solo la asignación de un bloque de tamaño apropiado y su liberación.
Prólogo
Epílogo
Cuando un subprograma termina, deben realizarse una serie de tareas de “mantenimiento”: devolver los resultados, liberar memoria. Igual que el prólogo, el traductor inserta este código al final del subprograma.
En un subprograma también se puede dar: - Sobrecarga