Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Pragmática: Son los problemas de construcción, todo lo hace al entorno, implementación, velocidad,
espacio, optimización. Es el estudio de los lenguajes que se ocupan de los problemas prácticos.
Imperativo o Estructurado: C – Pascal – Basic – Cobol
Orientado a Objetos: smalltack
Funcional: Miranda
Lógico: Prolog
Sintaxis: Estudia “como se escribe”. Es decir, definen como las sentencias pueden formarse con una
secuencia de símbolos. Utilizando estas reglas, puede determinarse si una sentencia es válida o no.
Ejemplo 1: en el lenguaje las variables empiezan con una letra y pueden seguir con letras y números.
Ejemplo 2: los verbos están entre el sujeto y el predicado. Ejemplo 3: cómo se escribe una iteración.
Sintaxis
Reglas Léxicas: El conjunto de caracteres que constituyen el alfabeto del lenguaje y la manera de
cómo se pueden combinar los caracteres para formar símbolos (tokens) válidos. Ejemplo 1:
controlan la ortografía. Ejemplo 2: PASCAL considera el uso de minúsculas y mayúsculas de manera
indistinta. En cambio C, las considera distintas.
Reglas Gramaticales o sintácticas: Como se escribe una sentencia, un programa o una parte del
mismo.
BNF: Es un metalenguaje. Que nos permite tener una definición compacta y clara de la sintaxis de un
lenguaje. Nace con Algol 60. Metalenguaje es un lenguaje que es usado para describir otros lenguajes.
No Terminales: Las “cosas que se definen” <exp><term>. Son entidades linguísticas compuestas
por un conjunto de símbolos, determinodos por las las relgas de sintaxis. Los no terminales se
encuentran del lado izquierdo y se definen del lado derecho. El símbolo de comienzo es un no
terminal.
Terminales: Las “cosas que no se definen” (números, letras). Deben ser símbolos válidos que
reconoce el lenguaje.
Limitaciones del BNF: No se pueden representar lenguajes naturales. No se puede expresar cotas
máximas y mínimas para números o longitudes. Ejemplo: Si quisiera definir que las constantes esten estre
2.256.256 y –2.256.256, tendría que generar una cantidad excesiva de reglas .
Conjunto de no terminales de las reglas léxicas: Son las constantes, palabras reservadas, variables,
etc. Estos son no terminales ya que se los puede expresar o definir en base al alfabeto del programa que
serian los elementos terminales.
Semántica
-1-
Binding (ligadura o atadura): Es el momento preciso en que se conoce una propiedad (tipo, valor,
alcance, almacenamiento) de un elemento (variable, constante, función, parámetro) de un lenguaje.
La información del binding se mantine en el descriptor.
Estático: Cuando no cambia durante la ejecución. Conozco la propiedad antes de ejecutar el
programa. Ejemplo: int x (va a ser una variable entera durante toda la ejecución).
Dinámico: La propiedad se conoce en tiempo de ejecución y puede ser cambiado, de acuerdo a las
reglas del lenguaje.
Variables
Caractristicas de las variables: Son el nombre, tiempo de vida, valor, tipo alcance, almacenamiento.
Binding de variables Estático Dinámico
Valor Constantes simbólicas Casi todas las variables
Tipo Mayoría (FORTRAN, ALGOL 60, COBOL, APL, LISP y SNOBOL4.
Pascal, ALGOL 68, SIMULA 67, CLU y Ada)
Alcance Mayoría (Fortran y Cobol) BASIC, APL, LISP y SNOBOL4.
Almacenamiento Fortran, Cobol Mayoría
Nombre: El nombre es usado para identificar y referirse a las variables. Algunos lenguajes permiten que
las variables no tengan nombres, llamadas variables anónimas. . Los punteros son el medio principal para
acceder a variables anónimas.
Tipo: Es una especificación de la clase de valores que pueden ser asociados con la variable, junto con las
operaciones que pueden ser legalmente usadas para crear, acceder y modificar tales valores. En algunos
lenguajes el programador puede definir tipos nuevos.
Binding Estático: El tipo es generalmente especificado por una declaración de variables. los
lenguajes con binding estático están orientados a la traducción.
Ejemplo, en Pascal : var x,y : integer;
z : boolean;
Ejemplo, en FORTRAN la declaración de la variable ALPHA, seguida de una sentencia tal como
ALPA=7.3, no sería detectada como un error; ya que se considera como la declaración implícita de
una nueva variable (ALPA).
Diferencia entre Pascal y FORTRAN: FORTRAN tiene reglas por default para determinar el
binding particular. Pero ambos asocian variables a tipos en tiempo de traducción y el tiempo del
binding es el mismo en los dos lenguajes.
Binding Dinámico: Las variables no son declaradas, sino que su tipo es implícitamente
determinado por el valor que actualmente contienen. Los lenguajes con binding dinámico están
orientados a la interpretación. El binding dinámico provee gran flexibilidad en la creación y
manipulación de las estructuras de datos. Los programas son difíciles de leer.
Ejemplo: Alcance dinámico en APL:
A<-17 {A adquiere el valor 17 y tipo entero}
A<-42.3 {A adquiere el valor 42.3 y tipo real}
A<-[2 1 4 7 2 6 -1 -2 -3] {A adquiere esos valores y se vuelve un arreglo de 3x3}
-2-
no puedo saber el tipo de una variable hasta el momento preciso en que se ejecuta.
Alcance: Ambiente, conjunto de sentencias que puede usar esa variable. Una variable es visible dentro de
su ámbito, e invisible fuera de el.
Binding Estático de Ámbito: Las variables tienen un lugar físico durante la ejecución, se usen o
no, y se quedan en memoria hasta que el programa termine. Define el ámbito de una variable en
términos de la estructura léxica de un programa. Lenguajes: Fortran y Cobol
Binding Dinámico de Ámbito: define el ámbito de una variable durante la ejecución del
programa. Lenguajes: BASIC, APL, LISP, y SNOBOL4.
Ejemplo: Alcance dinámico en Basic:
30 INPUT C
40 IF C<0 THEN GOTO 60
50 D=4
60 E=D+C
Si C=-3, no se conoce el valor de D => no se puede sumar; por lo tanto no sé si se puede usar el
valor de D en la sentencia 60. Solo cuando llego sé si se puede usar o no.
Clasificación de Lenguajes
Se clasifican de acuerdo a su comportamiento en tiempo de ejecución
Almacenamiento Estatico Lenguajes Estático
Almacenamiento Dinamico
Tipo y Alcance Dinámico Lenguajes Dinamico
Estáticos
Almacenamiento estático.
Son no recursivos.
Asociados a compiladores.
Ejemplo: Fortran y Cobol.
Estos lenguajes garantizan que los requerimientos de memoria para cualquier programa pueden ser
determinados antes del comienzo de la ejecución del programa. Por lo tanto, toda la memoria
necesaria puede ser ubicada antes de la ejecución del programa. Estos lenguajes no permiten la
recursión, porque podría requerir un número arbitrario de instancias y los requerimientos de
memoria no pueden ser determinados en tiempo de compilación (translation).
Tipo Algol (Basados en Pila)
Almacenamiento dinámico.
Alcance estático.
Tipo estático.
Asociados a compiladores.
Ejemplo: Algol 60, Algol 68, C, Pascal, Modula, Ada, etc.
Estos lenguajes permiten programas más potentes mientras los requerimientos de memoria no
pueden ser calculados en tiempo de compilación (translation). Sin embargo, el uso de memoria es
predecible y sigue un método del tipo LIFO .
Dinámicos
Almacenamiento dinámico.
Alcance dinámico.
Tipo dinámico.
Son recursivos.
Casi siempre relacionados a intérpretes.
Ejemplo: LIST, PROLOG, APL, SNOBOL4.
-3-
En estos lenguajes, como el nombre lo indica, no se puede predecir el uso de memoria. Estos
lenguajes no pueden ser modificados por un SIMPLESEM (A Simple Abstract Semantic Processor –
un simple procesador semántico abstracto) basado en stack, porque permite al programador crear
objetos en puntos arbitrarios durante la ejecución del programa.Cuando mas dinámicos son los
lenguajes mas difícil resulta hacer un compilador.
Modelo de ejecución
Compilador: Traduce completamente del programa fuente al lenguaje máquina, y lo transforma en un
ejecutable cuyo código difiere del programa fuente. Lenguajes Estáticos y Tipo Algol.
Intérprete: Entiende la instrucción. El programa fuente queda exactamente igual. No encuentra los
problemas de gramática. Ejemplo: conserva el texto del programa fuente tal como lo escribió el
programador. Dinámico casi siempre relacionados a intérpretes.
Lenguajes Lenguajes
Estáticos Tipo Algol
Programa Programa
ejecutable ejecutable
Todo de
datos tamaño fijo datos Sin tener en cuenta
memoria obtenida con
malloc, new o dispose.
frontera No ocupan tamaño
variable definido o fijo.
Durante la ejecución
de un programa puede
ser que no entre en
memoria.
No se usa
Lenguajes
Dinámicos
Intérprete
Programa
semitraducido
Tabla de
símbolos
Datos
-4-
Estructura de FORTRAN
Un programa en FORTRAN se compone de un juego de unidades, main y subprogramas. Se asegura la
cantidad de espacio que requiere cada variable local. Este se conoce tiempo de compilación y no se puede
cambiar en tiempo de ejecución. Cada unidad se compila separadamente y se asocia con un registro de
activación antes de la ejecución. Las variables se pueden crear antes del tiempo de ejecución y su tiempo
de vida se extiende sobre la ejecución entera del programa. El alcance de una variable se limita a la
unidad en la cual es declarada.
Las unidades pueden tener variables globales. Estas pueden ser vistas como que corresponden a un
registro de activación global a todas las unidades del programa.
Un programa debe pasar por ciertos pasos:
En el primero (compilación) cada unidad se traduce separadamente, sin conocer las otras unidades. Como
resultado todo se traduce a lenguaje de máquina, excepto que todas las referencias a memoria sean
compiladas de a pares (nombre de la unidad, offset).
En el segundo paso (enlace – linkeo) se enlazan las unidades. Como resultado todas las unidades se
asignan a una posición de memoria y todas las referencias a memoria se pasan a una dirección especifica.
El tercer paso (carga - load) consiste en cargar el programa en memoria y setear el punto de comienzo del
programa.
Describimos la semántica en términos de acciones especificas, lo que más nos interesa es la transferencia
de control entre unidades.
La primera posición de cada registro de activación se reserva para un puntero de retorno. La otra
información que se guarda son las variables locales (también debe contener información sobre los
parámetros).
Cada instrucción debe poder ser referenciada usando una pareja: el nombre de la unidad y el offset
(Desplazamiento).
En tiempo de enlace, como cada segmento se asigna a memoria, el par que refiere al registro de
activación o código de segmento debe traducirse a un número que es la dirección de memoria.
Como en el caso de FORTRAN, el tamaño del registro de activación y el OFFSET de cada variable local se
conoce en tiempo de compilación. El registro de activación no puede enlazarse al segmento de código de
la unidad estáticamente (antes de la ejecución) porque quizá se tomen acciones recursivas. Debe ser
enlazado para cada activación.
Anónimos:
Solo se ejecutan por sucesión, si la secuencia de control los encentra.
No pueden ser recursivos porque no se pueden ejecutar ellos mismos.
Un único nivel.
Las variables locales se crean un sola vez, pero se inicializan cada vez que se utiliza el
bloque.
No tiene registro de activación propio porque no tiene parámetros ni dirección de retorno.
Ejemplo: C
-5-
Ejecución de Lenguaje Tipo ALGOL
Cada vez que un bloque entra en ejecución su registro de activación se activa. Dirección de retorno del
que lo llamo es lo que necesita conocer un bloque. Los parámetros se almacenan como si fueran variables
locales del llamado, no del llamador.
Las variables locales se crean implícitamente cuando la unidad se activa, y la cantidad de memoria
requerida para mantener los valores de cada variable local se conoce en tiempo de compilación.
Registro de Activación:
Conjunto de datos necesitados para que un bloque se ejecute.
El registro de activación contiene variables, a veces parámetros o a veces dirección de retorno (si
es el mian).
Un bloque no puede tener un anidamiento parcial, tiene que estar si o si dentro de un bloque.
A Busca en el entorno local, luego en el global y así sucesivamente hasta encontrarla.
B
D
C
A F
A->B->B->B->E->D Cada vez que se comienza a usar un procedimiento se adquiere memoria para
las variables del mismo.
Cuando se empieza a ejecutar A, se crea un bloque de memoria que contiene las variables de A, y otras cosas. Este
bloque de memoria recibe el nombre de registro de activación (todo lo que necesita un procedimiento para poder
ejecutarse).
Cuando A->B, se crea un registro de activación de B, y se
Programa guarda en una pila de registros de activación y así
ejecutable sucesivamente. Las variables de A se ubican en el
primer lugar disponible y las variables de B se ubican
variables de A más a continuación de A.
otras cosas Cada vez que empieza a ejecutar un procedimiento se
variables de B
adquiere memoria para las variables del mismo.
Pila de registro de Cuando termina D las variables ya no requieren esa
D variables de B activación zona de memoria, quedando disponible dicha zona.
variables de B Cuando termina E las variables ya no requieren esa
zona de memoria, quedando disponible dicha zona.
variables de E Si termina B, y luego el otro B, el B que queda en el
variables de D ejecutable llama a D, (B->D), las variables de D se
instalan arriba del 2° B. (zona gris).Por eso se llama
asignación dinámica.
¿Dónde se almacenan los parámetros?, se almacenan como si fueran variables locales del llamado, no del
llamador.
-6-
Como en el caso de FORTRAN, el tamaño del R.A. y el desplazamiento de cada variable local dentro de él, son
conocidos en tiempo de traducción. En tiempo de traducción una variable puede ser ajustada sólo a su desplazamiento
dentro del R.A.; la asociación del espacio físico requiere conocimiento de la dirección del R.A., y esto sólo puede ser
hecho en tiempo de ejecución. Las variables de ésta clase se conocen como variables semiestáticas.
La reserva dinámica de los R.A. tiene dos consecuencias principales: permite la implementación de activaciones
recursivas de unidades y permite una utilización más eficiente del almacenamiento de datos. Los RA deben contener
suficiente información para identificar la instrucción a ser ejecutada y el RA que estará activo luego del retorno. Ahora
también reservaremos la posición en el desplazamiento 1 de cada RA para un puntero al RA de la unidad llamadora
(este puntero se llama el enlace dinámico). La cadena de enlaces dinámicos en el RA actualmente activo se llama
cadena dinámica y representa la anidación dinámica de las activaciones de las unidades.
Cuando una unidad U completa su instancia, su RA ya no se necesita más. Las reglas de tiempo de vida especifican
que cada instancia de U deben tener un nuevo RA. Las variables locales de U pueden ser visibles sólo a unidades que
están anidadas dentro de U y activadas luego de la activación de U. Estas unidades terminan sus activaciones antes de
que lo haga la instancia actual de U.
new(p)
El tiempo de vida de un objeto creado de esta forma, a diferencia de las variables semiestáticas y semidinámicas, no
termina cuando se termina el bloque que contiene la sentencia de alocación. Algunos lenguajes (PL/I) contienen
sentencias para desalojar tales objetos explícitamente. Otros lenguajes (Pascal) establecen que los objetos viven
siempre y cuando una referencia a ellos (un puntero) exista. Además es posible crear varios objetos sin desalojar
ninguno de ellos. Es fácil ver, por lo tanto, que tales objetos no pueden ser alojados en el stack.
Resumiendo: las variables dinámicas significan objetos de datos cuyo tamaño y/o número puede variar
dinámicamente durante sus tiempos de vida. Este hecho prohíbe la alocación de estos objetos en el stack: ellos se
alojan en un área de memoria llamada heap. El término heap denota libertad de la connotación de estrategia LIFO del
stack). Por esta razón, las variables dinámicas son a menudo llamadas variables heap, contrariamente a las variables
semiestáticas y semidinámicas, las cuales son llamadas variables stack.
-7-
En Fortran (lenguaje estático)
Traducción del nombre a una dirección fija. El dato está en un lugar conocido.
MOV R1,2413 i Mueve el contenido de la dirección 2413 al registro 1.
ADD R1,1490 j Suma el contenido de la dirección 1490 al registro 1.
MOV 2FAO,R1 h Mueve el contenido del registro 1 en la dirección 2FAO.
En Tipo Algol
Traducción del nombre a una distancia fija del comienzo al registro de activación. El dato está en un lugar
desconocido.
MOV R1,[R0] 15 i i va a estar 15 lugares después del registro de activación.
ADD R1,[R0] 40 j j va a estar 40 lugares después del registro de activación.
MOV [R0] 9,R1 h h va a estar 9 lugares después del registro de activación.
Cadena dinámica: Cada puntero apunta al inicio del procedimiento que lo llamó, sirve para saber donde
volver cuando un procedimiento termina. Cada registro de activación tiene un puntero al registro que lo
llamó, la sucesión de punteros se llama cadena dinámica. La cadena dinámica permite que las variables se
puedan utilizar sin importar donde están.
Cuando A->B se genera: ADD R0, tamaño de A
Cuando B->E se genera: ADD R0, tamaño de B
MOV R0,[R0] 8
C
B
B C BP
A
Suponiendo que el tamaño de B se encuentra en la dirección 12 y el offset de la cadena dinámica es 8:
MOV BX, BP
MOV AX, BP
ADD AX, [BP]12 (tamaño de B)
MOV BP, AX
MOV [BP]8, BX
Cadena estática: Conjunto de punteros donde cada uno apunta al registro de activación del
procedimiento que lo contiene en el árbol de anidamiento. Este tipo de estructura se usa para variables
globales.
No hay búsqueda porque no hay clave. Se accede siguiendo el puntero. (Pregunta de parcial).
-8-
Modelo de ejecución
A->B->B->C->D->E
Las variables se buscan en tiempo de compilación.
Para buscar una variable se busca en un entorno local y luego en uno global y así hasta encontrarla, sino
hay un error.
A
B
z A
x es variable global y está en C
z es variable global y está en A
C
x BD
AC
E B x:=y+z
Y Aes local
E y
C x
D
E
Cadena estática
Cadena dinámica
B
D
E
A esta secuencia de punteros se
la conoce como cadena estática,
B
está asociada a los anidamientos
D
y la cadena dinámica está
E
asociada al orden
A z
D
R0 (registro cero)
E
El compilador en tiempo de ejecución arma el árbol de anidamiento y entonces sabe donde está cada
variable. Las variables se las busca en tiempo de compilación.
Construcción de la Cadena Estática: La cadena estática se utiliza para cuando se trabaja con variables
globales. Si en un registro de activación no están las variables para resolver la instrucción entonces hay
que buscarlas en los registros de activación de los padres de éste.
Esta cadena une cada registro de activación con su padre. Para formar la cadena estática el copilador
forma el árbol de anidamiento de los registros de activación y cuenta cuantos arcos se necesitan para
llegar a una variable de uno de sus ancestros.
También el compilador guarda un offset que es la distancia de la base de cada registro de activación a la
posición donde está la dirección del padre (cadena estática).
-9-
A
Ejemplo:
El offset de X en un registro de activación es 12,
para Y es 4B y para Z es 20 C X
D B
Si se que se ejecuta x=z+y => z está 1 nivel atrás
y está 2 niveles atrás E Y
x está 3 niveles atrás
MOV BX, BP
MOV BP, [BP]6 F Z
MOV AX, [BP]20
MOV BP, BX
MOV BP, [BP]6 G X=Z+Y
MOV BP, [BP]6
ADD AX, [BP]4B
MOV BP, BX
MOV BP, [BP]6
MOV BP, [BP]6
MOV BP, [BP]6
MOV [BP]12, AX
MOV BP, BX
Variables
Semiestáticas: tienen tamaño fijo y lugar variable en distintas ejecuciones. Los limites (estáticos) no
varían no tienen porque esta en el R. A., los limites pueden estar en el código. Las variables solo
contienen datos. Pueden ser arreglos, punteros, estructuras, uniones. Ej: A[4]
Para el acceso a las variables semiestáticas, se requiere un offset a las variables semiestáticas.
Semidinámicas: tamaño variable y lugar variable en distintas ejecuciones. Son siempre arreglos con
limites variables. El tamaño varia en el momento preciso que se crea el R. A. y no varia mas. Las variables
contienen datos y los limites de los arreglos, que se los conoce como descriptores. Los limites
(descriptores) son semiestáticos no cambian de tamaño pero si cambian de valor. En los registros de
activación se separan los limites (Descriptores) de los datos. Ej: A[i]
Descriptores Constantes
var SD 4,17,8,22 DATOS En tiempo de ejecución
- 10 -
Lenguajes Tipo Algol
:
DATOS VAR SD 2
DESCRITOR VAR SD 2
DESCRITOR VAR SD 1
VARIABLES SEMIESTÁTICAS
El acceso a las variables semidinámicas requiere el acceso al descriptor para buscar el dato. La distancia
al descriptor la conoce en tiempo compilación, en tiempo de ejecución se conocen los datos. Variables
semidinámicas tienen más flexibilidad y consumen mayor tiempo.
Tamaño constante
En VD descriptor datos Variable
Registro de Activación
En VD
HEAP
Descriptores de VD Datos
Superdinámicas: Pueden cambiar de tamaño en cualquier momento y tienen ubicación variable. Tienen
tipos variables (solo se sabe en tiempo de ejecución). No se almacena ni la variable, ni su descriptor en el
registro de activación. No las soportan los lenguajes tipo pila como Algol, Pascal, C, C++. Existen solo en
los lenguajes dinámicos.
Prog. Semitraducido
- 11 -
Tabla de Símbolos
Leng. Dinamicos, el nombre esta en la tabla símbolos. El nombre
esta presente en tiempo de ejecución.
LISP, APL, SNOBOL4, PROLOG.
¿Qué tipos de variables usan los lenguajes orientados a la pila?¿y los dinámicos?
Lenguajes orientados a la pila --- VAR semiestáticas y semidinámicas. Descriptor de las Var Dinamicas.
Lenguajes dinámicos ------------- VAR superdinámicas.
¿Qué tipos de lenguajes poseen variables que pueden cambiar de tamaño en tiempo de
ejecución?
Los lenguajes dinámicos pueden tener variables que cambien de tamaño en tiempo de ejecución. Y
lenguajes tipo algol si utilizan variables semidinámicas o dinámicas.
Porque se dice que las variables semidinámicas solo pueden ser arreglos o estructuras mas
complejas.
Son siempre arreglos (o estructuras mas complejas) con limites variables. Y estos limites (descriptores)
son semiestáticos no cambian de tamaño pero si cambian de valor. Entonces las variables semidinámicas
tienen tamaño variable y lugar variable en distintas ejecuciones. Ej: A[i]
- 12 -
¿Quién se encarga de asignar el espacio en memoria de las variables que se almacenan en el
HEAP, para Pascal y Algol?
En Algol el que se encarga de otorgar espacio en memoria es el RUNTIME PACKAGE. Y en Pascal el que se
ocupa de asignar espacio es el usuario.
¿Qué diferencia tienen las variables dinámicas de Pascal con las de Algol?¿Qué similitud?
La diferencia es que Pascal tiene compatibilidad de tipo por nombre y Algol por estructura.
La similitud es que tienen alcance y tipo estático, cambian de tamaño en tiempo de ejecución.
Almacenamiento dinámico, no ocupa un tamaño fijo en memoria.
¿En que casos el nombre de una variable está presente en tiempo de ejecución?
Cuando esta se trata de variables superdinámicas. Esto es así ya que existe una tabla de símbolos para
mantener y administrar este tipo de variables en memoria.
¿Los limites de los arreglos están en algún lugar de memoria en tiempo de ejecución?¿El
lenguaje verifica limites?
El programa tiene los valores guardados en algún lugar de memoria, entonces el lenguaje verifica límites.
Si el lenguaje no verifica límites, no lo guarda, después lo interpreta.
Si el lenguaje verifica limites, y hablamos de variables semidinámicas, el ejecutable necesita conocer
todos los limites (porque puede cambiar el R.A.). Matriz (4 limites), array (2 limites).
Si el lenguaje verifica limites, y hablamos de variables semiestáticas, el ejecutable necesita conocer
para Array 1 limite (limite inferior) y para Matriz 3 limites.
Ej Z [7..45,9..22] - Por Filas (7,9,22) - Por Columna (7,45,9)
q22,8 ………….…………q22,17
la dirección de q [i,j] es igual a la dirección de q [4,8] por que por filas o por columnas siempre es el
primero.
dirección q [i,j] = dirección [4,8] + tamaño de cada elemento x [(i-4) * (17-8+1) + (j-8)]
cantidad de filas
cantidad de elementos de cada fila
cantidad de elementos que hay antes en la fila
- 13 -
Matriz organizada por columnas
dirección q [i,j] = dirección [4,8] + tamaño de cada elemento * [(j-8) * (22-4+1) + (i-4)]
Necesito conocer los dos comienzos y 1 final (4,22 y 8).
Los bloques anónimos de “C” {} no requieren un registro de activación propio, pese a que
tienen variables locales.
Verdadero. Los bloques anónimos no tienen nombre, por lo que no tienen parámetros, ni dirección de
retorno. Por consecuencia, no pueden ser recursivos. No voy a tener nunca dos registros de activación
para un bloque anónimo. No necesito registro de activación propio, porque al no repetirse, puede estar en
el registro de activación de otro archivo “.C”. Esto hace al programa más eficiente. Si tendría un registro
de activación por cada FOR o WHILE, ocuparía más lugar y tendría más accesos a memoria. La mayoría de
los C no asignan registros de activación por eficiencia.
El tiempo de vida de una variable declarada en un programa en lenguaje Pascal, sin llamadas a
procedimientos ni funciones, coincide siempre con su alcance.
Verdadero, no tiene procedimientos ni funciones. El tiempo de vida no tiene nada que ver con el alcance.
- 14 -
char f[a];
int c[a][3];
int c[a][3] :={205,306,405,101,125,115};
d:=(int *) malloc (200);
I:=(int *) malloc (100);
}
Tipo: De acuerdo a las operaciones del programa se puede decir que todas las variables tienen tipo
estatico (no cambian durante la ejecución).
Alcance: Todas las variables son de alcance estático ya que se sabe que instrucciones pueden hacer uso
de cada una de ellas (tanto si son globales como locales).
Valor: El valor de todas las variables es dinámico, es decir, se conoce en tiempo de ejecución.
Almacenamiento: Como el lenguaje permite variables semidinamicas “f[a]” el almacenamiento debe ser
dinámico ya que las posiciones tienen que poder variar de un ejecución a otra.
Para el caso de los punteros (int *c, *d, *j, *i), el puntero es una variable semiestatica (está en la pila, no
puede cambiar de tamaño pero se de posición de una ejecución a otra) pero el bloque de memoria en el
HEAP apuntado por el puntero.
Una variable declarada en C ó C++ como static posee almacenamiento y alcance estáticos.
Verdadero, tiene alcance y es estático.
Si una variable definida en el interior de un bloque de “C” {} contiene una inicialización, por
ejemplo, int x=3; la misma se ejecuta cada vez que comienza el bloque.
FALSO.
Bloque anónimo: La variable se crea una sola vez, pero se inicializa cada vez que se utiliza el bloque.
Bloque con nombre: Se crea y se inicializa cada vez que se llama al bloque.
Las inicializaciones en “C” solo se ejecutan cuando se crea el registro de activación.
FALSO.
En los bloques anónimos en C, las variables se crean una vez y se inicializan muchas veces.
¿En que casos el puntero de la cadena dinámica apunta al mismo lugar que el puntero de la
cadena estática?
Cuando cada padre llama a un hijo y este a otro y así sucesivamente. Es decir llama a alguien en forma
local.
- 15 -
Cada puntero apunta al inicio del procedimiento que lo llamó.
¿Para que se usa?
Sirve para saber donde volver cuando un procedimiento termina. La cadena dinámica permite que las
variables se puedan utilizar sin importar donde están.
Porque los lenguajes con alcance dinámicos necesitan un tabla de símbolos en tiempo de
ejecución.
Los lenguajes con alcance dinámicos define el ámbito de una variable de acuerdo a la ejecución del
programa. Dicha tabla tiene la información de las variables del programa, como nombre, tipo, y los
punteros que apuntan al área de datos.
Explicar porque el uso de variables semidinámicas es más lento que el uso de variables
semiestáticas.
A las variables semidinámicas hay que acceder primero al descriptor, quien contiene los límites y un
puntero a la variable.
Explicar porque el uso de variables globales de la pila es más lento que el uso de variables
locales.
Porque el alcance de las variables locales es mas chico que el alcance de las variables globales.
¿Es también para las variables estáticas?
Pasaje de parámetros
Es la transferencia de datos entre un método invocado y un método/procedimiento invocador.
Tienen ventajas en términos de legibilidad y modificabilidad. La mayoría de los LP usan un método posicional para
asociar los parámetros actuales a los formales en las llamadas a los subprogramas.
Tipos de parámetros:
Formales: Son aquellos que se utilizan en el momento de definición.
Ejemplo:
int pepe (int x,int y)
variables locales de la función,
aunque de alguna manera provienen de afuera.
Reales: Datos del llamado que se entregan en cada ejecución.
Ejemplo:
pepe (3,b)
parámetros reales
Sintaxis del pasaje de parámetros: ¿Quién con quien? El pasaje de parámetros es la asociación de los parámetros
formales y reales.
Por Posición
Completa
Faltante
Al final.
En cualquier lado.
Anónimas: Son aquellas en que no digo cuales van a ser los parámetros.
Ejemplo: int pepe (float x, …)
int printf (char * fint, …)
pepe (7.2)
pepe (7.3, ‘b’, 22, “texto”, …)
se pueden poner muchos o ningún parámetro.
printf (“%s, …%d”, x,y);
printf (“%s,%d”, x,y,z); //Solo imprime X e Y.
Semántica del pasaje de parámetros: Como se asocia (no quien se asocia con quien)
(LLAMADOR) A->B (LLAMADO)
B
A x
Referencia: B(x), existe un solo x y está en A (llamador), el parámetro real es el único que existe, y el formal no
existe, solo es otro nombre para x (alias o sinónimos). La unidad llamadora pasa a la unidad llamada la dirección
del parámetro actual, se trata como una referencia a la posición cuya dirección se pasó. Estándar de Fortran.
Pascal permite pasar valores por referencia.
Nombre: El mecanismo es muy similar. Tampoco existen 2 parámetros (copias textuales). Cada ocurrencia del
parámetro formal se reemplaza textualmente por el parámetro actual. Estándar en Algol 60.
Copia: Existen 2 parámetros, el real y el formal. Ambos tienen ubicaciones distintas y están en registros de
activación diferentes.
B z
A x
Los parámetros formales no comparten almacenamiento con los parámetros actuales; sino que actúan como variables
locales. Así, la llamada por copia protege a la unidad llamadora de modificaciones inadvertidas de los parámetros
actuales.
El pasaje de parámetros por copia tiene tres variantes:
Por valor: Se utiliza a la ida cuando los datos van del llamador al llamado. La unidad llamadora evalúa los
parámetros actuales, y estos valores se usan para inicializar los parámetros formales, los cuales actúan
como variables locales en la unidad llamada. La llamada por valor no permite ningún flujo de información
de retorno al llamador, así que las asignaciones a los parámetros formales no afectan a la unidad
llamadora. Pascal permite pasar parámetros por valor.
Por resultado: Se copia a la vuelta, la unidad llamada termina y devuelve el dato. las variables locales
correspondientes a los parámetros formales no son seteadas en la llamada al procedimiento, pero su valor,
al retorno, se copia en la posición del parámetro actual en el ambiente del llamador. La llamada por
resultado no permite ningún flujo de información a la unidad llamada.
Por valor/resultado: Se copia a la ida y a la vuelta. , las variables locales que denotan parámetros
formales son inicializadas en la llamada al subprograma (como en la llamada por valor) y al retorno,
copian sus valores a los parámetros (como en la llamada por resultado). La llamada por valor-resultado y
la llamada por referencia pueden tener efectos diferentes.
Programa modelo:
a:array[15] int;
h:int; //h es global.
procedure parte 1 (x, y:int)
h:=h+1;
- 17 -
x:=x+1;
print (“H”, h, “X”, x, “Y”, y);
fin
{main}
h:=1;
a[1]:=2, a[2]:=3, a[3]:=4;
parte1 (h, a[h]);
print (“h”, h, “a[h]”, a[h]);
1. Referencia:
Pasa como parámetro la dirección de memoria, entonces la variable local queda apuntando a esa dirección de
memoria.
h-> 1 2 3
a[1]->2
2. Por nombre:
Reemplazo el nombre dentro de la función. Primero se hace un reemplazo textual y después se ejecuta.
h=h+1 2
h=h+1 3
h, h, a[h]
3, 3, 4 -> parte1
3, 4 -> main
3. Copia resultado:
Primero se inicializan los parámetros en cero. Y una vez terminada la función se copian los resultados de los
parámetros formales hacia los reales.
h=h+1 2
x=0+1 1
4. Copia valor:
Copia parámetros formales con el valor de los parámetros reales.
h=h+1 2
x=1+1 2
El vector quedaría:
2,2,2 -> parte1
2,3 -> main a[1]=2 a[2]=3 a[3]=4
5. Copia valor-resultado:
Hace ambas cosas.
- 18 -
Copia parámetros formales con el valor de los parámetros reales.
h=h+1 2
x=1+1 2
En los pasajes por copia que devuelve resultado (copia resultado y copia valor-resultado), tiene el siguiente error:
{main}
h:=1
parte 1(a,a)
procedure parte 1(x, y) Error, hay dos valores resultado
x=x+1; para a y no se sabe cual tomar.
y=y+3;
SUBRUTINE ZZ (I,J)
I=22
END
M=8
ZZ (M,N) ZZ (12,N)
K=M //K vale 22 K= 12*2 //24
L=12 //22
“L” resive el valor 22 porque altera el lugar donde esta el 12.
Porque las constantes tienen un lugar fijo en memoria y
si las altero, guarda el último valor.
ZZ(8,N)
K=8 //K vale 22
zz (12,h)
12:=12+h //error ya que el compilador de Algol no me deja poner constantes a la
//izquierda.
zz (h,12)
h:=h+12 //OK
- 19 -
Introdujo un nuevo problema que hace muy difícil descubrir los errores.
Tipos de datos
Producto cartesiano:
Es un juego de elementos ordenado por tuplas. Puedo definir I*R*C (entero*real*carácter) => es una variable
válida.
En Pascal son los record. En C son los struct.
La mayoría de los lenguajes tienen la posibilidad de tener producto cartesiano.
Arreglos:
Una sucesión de tipo de valores donde se le asocia un número entero que actúa como índice. Usa un índice con un
valor que no pertenece al dominio.
En Ada se acepta un mapeo de las enumeraciones sobre otro tipo. Ejemplo: Gastos [mar] lun, mar, mierc.
Enlace de una sucesión a un tipo:
- 20 -
En tiempo de compilación: soporta variables estáticas y semiestáticas. Ejemplo: FORTRAN, C y PASCAL.
En tiempo de creación: se hace en tiempo de ejecución cuando se crea la variable. Son arrays
dinámicos. Estos son de tipo semidinámicos (¿?). Ejemplo: ALGOL 69, SIMULA67.
En tiempo de uso: es el más flexible y el más costoso. El tamaño del array puede cambiar en tiempo de
ejecución. Es típico de lenguajes dinámicos como SNOBOL4 y APL.
Secuencias:
Ejemplo: STRING, FILE. Un número de ocurrencias seguidas de un cierto tipo de datos. Difícil manipulación y
problema de almacenamiento (dinámico), porque no se conoce su tamaño.
Recursión:
No se invoca n veces, sino una sola. Pueden contener componentes que alarguen el mismo tipo.
Ejemplo: Un árbol binario. Una lista doblemente enlazada usando nodos.
{typedef struct pepe
int x;
char z;
Uniones:
Agregado de datos producido cuando 2 o más datos ocupan el mismo lugar en diferente momento.
¿Para qué sirven las uniones? Hace mas flexible el código.
Ahorro de espacio, memoria.
Dar soporte de tipos dinámicos en lenguajes tipo Algol.
Ejemplo:
unión (int,float) x; (lo que digo es que x puede cambiar a int ó float y nada más).
En Pascal: A las uniones se las llama registros variantes. Estos tienen discriminantes, la variable es explícita, las
uniones en Pascal son inseguras, puedo guardar un real teniendo guardado un entero.
En Algol: A las uniones se las llama uniones. Estas uniones tienen discriminantes y este discriminante es implícito, el
programador no lo ve. Unión segura. El compilador no me deja equivocar.
En Ada: Las uniones son registros variantes que tienen discriminante y éste es explícito y se trata de una unión
segura.
En C: Son uniones, no tienen discriminante y son uniones inseguras. Escribo un discriminante o nó según el
programador quiera.
Ejemplos:
En Algol: unión (int,real) x;
En C: unión (int x,real y) z;
_El compilador reserva 4 bytes (2bytes para int y 4 bytes para real).
_Si hay un int desperdicio una parte.
_Si hay un real uso todo
En Algol reserva 5 bytes, 4 para el real o el int y 1 para el discriminante (pista del tipo almacenado).
En Algol: z:=2 i 2 a:=x las variables unión no pueden estar del lado derecho.
Cláusula de conformidad de Algol: Permite al lenguaje tener uniones seguras. Para usar un dato de una unión se
interroga sobre el tipo de dato. Solo se puede usar la unión (a la derecha) dentro de una clausura de conformidad
CASE. Las uniones por si solas son inseguras. Por eso Algol verifica los tipos de datos.
Ejemplo: Sumar una unidad a una unión en Algol
unión (int,real)x;
- 21 -
case x in
when int x: x:=x+1
when real x: x:=x+1.0
esac
Unión en PASCAL
record producto;
número:integer; parte común
pventa:real;
discriminante explícito: lo tengo que
escribir.
case stock: bolean of
true: (cant:integer);
false: (ordenado:real, unión Parte variante con un discriminante
esperado:real) lamado stock (representa la unión)
end
p1:producto;
p1.número:=10;
Me indica que las uniones son
p1.stock:=true;
p1.ordenado:=3.5; uniones inseguras inseguras en Pascal ya que uno
tendría que guardar una cantidad
entera y no una real.
Si un lenguaje tiene discriminante obligatorio en sus uniones, las uniones son seguras.
FALSO. Pascal utiliza discriminante explicito que representa la unión y las mismas son inseguras.
p1:=( , ,TRUE,7);
Es correcto ya que el compilador
p2:=( , ,FALSE,7.3,8.5);
p3:=p1; verifica que toda la parte variante
sea consistente.
El compilador verifica que la parte variante sea consistente, siempre se maneja con registros variantes consistentes,
por lo tanto las uniones en ADA son seguras. Nunca se acepta un valor inconsistente.
El discriminante puede ser cualquier enumeración, nó únicamente booleano.
Registro variante congelado en ADA: No puede cambiar de variante en ningún momento de la ejecución, al
discriminante se le da un valor fijo.
p4:PRODUCTO(false);
Cuando se declaran variables hay que especificar obligatoriamente el valor del campo discriminante (excepto en la
declaración de parámetros formales), una vez hecha la declaración no se puede cambiar.
- 22 -
WHEN FALSE => x: INTEGER;
END CASE
END RECORD
Entonces, para cambiar: x => (t=>TRUE; x=>3,4) (en la misma instrucción!)
¿Cuáles son las limitaciones para el uso de registros variantes congelados en ADA?
No puede cambiar de variante en ningún momento de la ejecución, al discriminante se le da un valor fijo. Si cambio el
discrimínate tengo que cambiar el dato.
¿Puede una variable de tipo unión pasarse como parámetro, si se pasa por referencia?¿Y por nombre?
Puede pasarse por referencia, pero hay que tener cuidado con el uso que se le da en el procedimiento, porque las
uniones no pueden ser usadas del lado derecho de una asignación(Exepto en Algol si uso la Cláusula de
Conformidad).
El pasaje por nombre también es válido pero hay que tener cuidado dentro del procedimiento como se usa.
Asignaciones
a:=b En C -> edevalio:=errevalio
valor (tiene que ser del mismo tipo de la celda apuntada por la dirección)(evalue).
dirección (lvalue)
Según Algol: Las variables son referencias a celdas. Cuando mencionamos la variable hablamos de la dirección y nó
del contenido.
x es la dirección de un entero.
y es la dirección de un puntero a entero el cual a su vez tiene una dirección de otra celda que es un entero.
z es la dirección de una celda que a su vez es un puntero a puntero de entero que contiene una celda la cual es un
entero.
x, y, z son variables de distinto tipo, en las que difieren que unas son enteros y otras son punteros.
x int
y
puntero a int int
z
puntero a puntero a int puntero a int int
Asignaciones en ALGOL
x:=y; copia un entero (2 desreferencing)
x:=z; copia un entero (3 desreferencing)
y:=x; copia un puntero a entero (0 desreferencing)
y:=z; copia un puntero a entero (2 desreferencing)
z:=x; copia un puntero a puntero a entero (MAL)
z:=y; copia un puntero a puntero a entero (0 desreferencing)
- 23 -
En Algol cuando el lado derecho tiene un valor que no satisface las exigencias del lado izquierdo, entonces se hacen
extracciones de valor, la cantidad necesaria.
Extracciones de valor:
Seguir un puntero (hasta encontrar uno que sirva).
Extraer valor.
Desreferencig.
Asignación en C:
x:=y; (2 desreferencing) x=*y;
x:=z; (3 desreferencing) x=**y;
y:=x; (0 desreferencing) y=&x; (& no me da el dato, sino me da una dirección)
y:=z; (2 desreferencing) y=*z;
z:=x; (MAL) MAL
z:=y; (0 desreferencing) z=&y;
En C existe una extracción de valor implícita fija, si se necesitan más hay que escribirlas, por eso para el
desreferencing se debe hacer *y. Cuando no hay desreferencing en C se utiliza & que se suele denominar extracto de
desreferencing.
En C las variables también son direcciones como en Algol.
Si quiero colocar en int un 4 para y =>
Casting en Algol: En Algol quiere decir seguir un puntero del lado izquierdo de una asignación.
Int x;
ref int c;
Casting: es un puntero y referencia a la primer celda.
c:=2;
(ref int) y:=x;
Ejemplo 2:
float a;
*((char *)(&a) + 3)=’z’
‘z’
&a bytes
Control de precisión
Aritméticas para representar números. Que utilizan los diferentes lenguajes para representar variables reales. Hay dos
aritméticas básicas (BCD y base 2 (IEEE)).
float a=0.0
for ( ; a!=1.0; a=a+0.1) ¿Cuántas veces imprime a?
{ printf (“%f”,a); }
Cuando compilo con bcdlib (BCD) imprime “a” 10 veces y con stdlib (IEEE - Base2) +∞.
- 24 -
float a=0.1
if (a*10.0==1.0)
printf (“que bien”);
else ¿Qué imprime qué bien o que mal?
printf (“que mal”);
Ejemplo: 1212.33
1 2 1 2 3 3
Ventajas:
10*0.1=1
Más preciso
decimal binario
6 110
5.5 101.1
5.25 101.01
4.75 100.11
1/10 0.1
5 1 0.25 *2
2 0 0.50 *2
1 1 1.00 *2 (cuando nos dá 00 => fin)
Desventajas:
10*0.1≠1
No tiene precisión
Ventajas:
Más económicas.
Más rápidas.
Aprovecha mejor el espacio
- 25 -
Cobol (BCD).
Fortran (Formato propietario IBM, luego IEEE).
Pascal (IEEE).
C (Se elige para todo el programa, Link-editor).
Ada (Se elige para cada variable).
Algol (Se elige para cada variable).
Ejemplo:
¿Cuántas y cuáles son las formas que tienen los lenguajes para controlar la precisión de las variables
reales?
Los lenguajes tienen dos formas de controlar la precisión de las variables reales:
Es lo establecido por la IEEE en la cual se tiene mantiza y exponente para expresar el número. El límite es el tamaño
del tipo real. Algunos lenguajes utilizan BCD para expresar estos números.
Por ejemplo, en ADA se fija la precisión y se utiliza BCD para representarlos.
Conversiones de datos
Conversiones Explícitas: El lenguaje exige que se escriban las conversiones con claridad. Dificulta la escritura, pero
facilita la lectura (mantenibilidad). Se utiliza en ADA.
Conversión Explicita:
REAL X;
INT Z;
X:=REAL(Z);
¿Cómo son las conversiones entre tipos en ADA? ¿Qué consecuencias trae?
Las conversiones en ADA son explícitas. Esto trae la consecuencia que se deben “castear” las variables de diferentes
tipos antes de una asignación. hace que el lenguaje sea más legible y mantenible, aunque hay que escribir más.
Conversiones implícitas: Las conversiones se hacen automáticamente. Se hace una mezcla de tipos. Dificulta la
lectura (mantenibilidad) y facilita la escritura. Se utiliza en C, Algol, Pascal, Fortran.
Conversiones implícitas en C: El compilador hace todo el esfuerzo de acuerdo con la expresión y el tipo de variable
involucrada.
char short int int long int
float double long double
- 26 -
COMPLEX DOUBLE
Se transforma Complex
en real Double
B=B*D+A+C REAL
REAL
COMPLEX
INTEGER
COMPLEX DOUBLE
Desproceduring: Asigna el valor devuelto por un procedimiento a una variable. Cuando el lado derecho es un
procedimiento y el izquierdo es un tipo numérico => se ejecuta el procedimiento (desproceduring).
cuerpo del procedimiento: instrucciones que lo componen.
Si en Algol escribo: proc xx (---) -----
-----
-----
proc zz (---) -----
-----
-----
proc temp;
temp:=xx;
xx:=zz; dieron vuelta los cuerpos de los procedimientos, en C esto fue parecido a punteros a funciones.
zz:=temp;
El cuerpo del procedimiento xx se copia en el procedimiento temp. Cuando el lado derecho es un procedimiento y el
izquierdo también => se copia el procedimiento.
En Algol los procedimientos se copian.
u:=v (Si u es un número y v es un procedimiento => se debe ejecutar el procedimiento que devuelve el número que
se necesita).
Rowing: Asignar un valor a un arreglo. Si no se especifica posición se almacena a todas las filas.
[4:12] int x; x es un arreglo de enteros.
x:=x[6]; para todos los componentes del arreglo adquiere el valor que exista
en la fila número 6.
Widening: Un valor de un tipo, puede ser asignado a una variable de tipo más amplio.
- 27 -
Voiding: Un valor puede ser asignado a una variable nula, perdiéndose el valor.
Uniting: Cambia automáticamente el discrimínate según el tipo de dato a asignar. Algol no permite uniones del lado
derecho.
Compatibilidad: Para algunos lenguajes dos variables son compatibles cuando el tipo de datos y para otros cuando
tiene la misma estructura. Se usan cuando en una operación pueden participar dos tipos de datos.
En C
Typedef pepe int;
pepe x;
Int y;
Para C “x” e ”y” son compatibles porque tienen la misma estructura.
Para Pascal y ADA no son compatibles porque pepe <> int.
En Ada la compatibilidad es el nombre, si el nombre del tipo son los mismos, ahí dos variables son compatibles.
LADO : INTEGER
SUMA : FLOAT
SUMA = LADO no se puede hacer, para ADA son incompatibles.
SUMA = FLOAT(LADO) se realiza la conversión explicita para poder operar.
Algol tiene compatibilida por estructura, para igual tipo de variable. Uso de conversiones implícitas. Por ejemplo
desreferencing y windening.
¿Qué diferencia existe entre las compatibilidades entre tipos de ADA y Algol?
En ADA la compatibilidad es por nombre, o sea, todas las variables del mismo tipo de datos se pueden usar en
operaciones. Las que tengan igual estructura pero distinto nombre son incompatibles para ADA. Usa conversiones
explicitas.
Algol tiene compatibilidad por estructura. Para igualar tipos de variables y hace uso de conversiones implícitas.
ADA tiene la ventaja de tener buena legibilidad y matenibilidad pero se debe escribir mucho.
Algol mejora la escribilidad pero es difícil de leer y de escribir.
¿Que características tiene que tener un lenguaje de programación para ser confiables?
Legibilidad: Propiedad que dice cuanto fácil o difícil es leer un programa.
Mantenibilidad: Propiedad que dice cuanto fácil o difícil es mantener un programa.
Escribilidad: Propiedad que dice cuanto fácil o difícil es escribir un programa.
Legibilidad con mantenibilidad se llevan bien y legibilidad con escribilidad, enemigas.
Instrucción en APL:
- 28 -
Dificultades con los punteros
Los primeros punteros tenían los siguientes inconvenientes:
En PL1 =>
DECLARE P POINTER;
DECLARE X FIXED; Problemas con el tipo de dato de la celda apuntada
ALLOCATE X SET P;
Declara “X” como un entero, aloja memoria para “X” y hace que apunte a “X”.
1) En PL1 no sabemos a que tipo de dato apuntan los punteros. Esto trae problema con el tipo de dato de la celda
apuntada. Se puede estar usando mal el dato que se apunta.
2) El segundo problema es lo que se denomina punteros colgados o dangling reference que surge cuando un puntero
apunta a una celda de memoria cuyo contenido no se puede garantizar.
Regla de alcance de Algol: En toda asignación en la que se copian direcciones el alcance del lado izquierdo debe ser
≤ que el alcance del lado derecho. El apuntador no puede vivir menos el apuntado, sino lo mismo. El apuntador no
puede sobrevivir apuntando a algo que no existe(regla de alcance). Esto previene el dangling pointer o dangling
referente.
Algol prohíbe que halla punteros colgados.
Ejemplo 1
begin
ref int rx, int x;
begin
ref int ry, int y;
rx:=x; //Mismo alcance y tiempo de vida.
rx:=y; //ERROR cuando muere la función muere la variable “y” y “ry” apunta a algo que no existe.
ry:=x; //El alcance de “ry” es mas reducido que el alcance de “x” que desaparece primero.
ry:=y; //Mismo alcance y tiempo de vida.
end
Escribir un ejemplo que refleje como ALGOL evita los punteros colgantes (dailing reference) utilizando su
regla de alcance y describa que pasa en la misma situación en Pascal.
- 29 -
END
En ALGOL, el alcance del referenciador tiene que ser menor o igual al del referenciado.
Los punteros en PASCAL no dan lugar a tales problemas porque solamente se pueden limitar a UNNAMED :
referenciar anónimos data objects, que son almacenados explícitamente al hacer NEW.
La Instrucción NEW de PASCAL, asigna espacio de memoria para el almacenamiento necesario de una variable
dinámica (ejemplo: una palabra).
A medida que NEW va asignando espacio en el HEAP 1, el espacio disponible se va reduciendo. El puntero que apunta a
la cima del HEAP, va subiendo por las direcciones de memoria, reduciendo cantidad de memoria disponible para otros
valores. Esto puede provocar que la memoria del HEAP se agote, lo que produciría un error al intentar crear una
variable dinámica.
En C:
C, como la mayoría de los lenguajes permite que halla punteros colgados.
Los tres inconvenientes con los punteros son:
1) Se utiliza mal el puntero. Acceso a celda confundiendo el tipo.
2) Puntero colgados:
Ejemplo 2
p=(char *)malloc(100);
if (a<b) return(); // No libera memoria (Garbage).
else free(p) //Libera memoria (Fragmentación).
Ejemplo 1
int *p;
P = (int*)malloc(100);
P = (int*)malloc(200);
¿Y la fragmentación?
El programador no puede evitar la fragmentación, ya que si bien puede liberar correctamente los punteros, los bloques
de memoria quedaran libres, fragmentando memoria. Es tema del sistema operativo o del lenguaje si administra el
HEAP.
Los lenguajes dinámicos hacen compactación de memoria.
¿El programador puede evitar la generación de garbage en los LTA (Lenguaje Tipo Algol)?
El programador puede evitar la generación de garbage, o sea la generación de bloques en los cuales no hay punteros
que le apunten a él. Esto se puede hacer liberando la memoria cuando la variable puntero va a hacer referencia a otra
variable. Ejemplo:
int *p, *q;
p = (int *) malloc (100);
q = (int *) malloc (200);
free (p);
p =q;
Evitando allocar espacio en memoria para la misma variable.
- 30 -
Administración del Heap
Fragmentación: Con la creación y eliminación de variables dinámicas se producen huecos, que en algún
momento se ocuparán o nó. Estos huecos se producen porque el programador puede cambiar el lugar y el tamaño de
las variables. (Similar a la fragmentación de disco)
Garbage o basura: Zona de memoria inutilizable por el programa, para crear otras variables. La basura se encuentra
en el Heap. Es sólo propiedad de los lenguajes tipo Algol.
En los lenguajes estáticos no es posible que exista garbage porque tiene almacenamiento estático. Estos lenguajes
garantizan que los requerimientos de memoria antes del comienzo de la ejecución del programa. Por lo tanto, toda la
memoria necesaria puede ser ubicada antes de la ejecución del programa.
Lenguajes dinámicos: Lenguajes dinámicos todas las variables accedidas desde la tabla de símbolos.
Intérprete
Programa
Cuando la memoria llena está fragmentada se comporta como el
garbage.
Tabla de símbolos Necesito poner los espacios ocupados de memoria en un mismo
lugar, uno a continuación del otro para generar más espacio en
memoria (compactación de memoria). Defragmentación.
Heap Punteros
Defragmentación (compactar la memoria): Se recorre la tabla de símbolos buscando el puntero que tenga la
dirección más alta y corro los datos hasta al final, cambio el puntero (en la tabla de símbolos) apuntando al lugar en
donde quedó el dato. Así sucesivamente, compactando los datos en un extremo y en el otro los espacios libres de
memoria.
- 31 -
Garbage collection: Se utiliza para resolver el garbage o basura. Las variables están dispersas en el Heap. Los
punteros los administra el lenguaje, no el usuario. Hay porciones de memorias no usadas.
En memoria virtual el garbage collection casi no ocurre.
En memoria real el garbage collection ocurre frecuentemente.
Lenguajes tipo Algol: Lenguajes tipo Algol variables dinámicas accedidas desde la pila de registros de
Activación.
Ejecutable
Pila
U
Heap
Ejecutable Pila R.A.
A B C D L4 U5 L3 U4 G2 L2 G1 U3 U2 U1 L1
L
El garbage collection se hace cuando necesito memoria, se crea un árbol, pero para crear el árbol necesito memoria.
Veremos un algoritmo de recorrido del árbol utilizando prácticamente nada de memoria (recorrido recursivo):
Cada vez que se transita un puntero, antes de irme pongo un puntero al padre, cada vez que avanzo pongo un
puntero al padre, cuando llego a la hoja retrocede teniendo todo el camino.
Siempre voy a tener un lugar para poder volver. Es como poner una marca para saber por donde pasé.
- 32 -
Raíz
U3
U4 U2
U1
Luego viene la desfragmentación: Se borra el primer usado en la cabecera de usado y se lleva al final de la
memoria. Luego se busca todos los punteros que apuntan a este puntero. Luego se repite la operación.
Hay una estrategia para disminuir el costo de este proceso que se denomina estrategia de contador referencia.
Cada objeto tiene un contador de referencia.
Cada puntero sabe cuantas variables está apuntando.
Cada variable sabe los punteros que la están apuntando.
Ejemplo:
p y q son 2 punteros.
p=q;
resto uno sumo uno
Cuando un bloque tiene su contador de referencia en cero, se borra impidiendo la necesidad de un algoritmo
complicado.
R1
1 1
- 33 -
Contador de referencia
¿Cuáles son las condiciones para que un elemento del n heap sea devuelto a la cadena de libre utilizando
contador de referencia?
En el algoritmo de conteo de referencia cuando hago “malloc” o “free” este algoritmo reserva un pedazo en el heap
(celda), que es el contador de referencia. Cuando hay un puntero en el heap cuyo contador de referencia es igual a
cero, ese pedazo de memoria está libre.
- 34 -
Parsing Ascendente (LR o SLR)
Se basa esencialmente en la operación de reducción, en reemplazar el lado derecho por el lado izquierdo
de las reglas. Durante el desarrollo del parsing ascendente trabajaremos con la gramática a continuación
descripta, la hacemos recursiva a derecha y luego la factorizamos para ver cual es la acción que nos dice
el terminal que hay que hacer. El PA no requiere modificaciones esenciales.
1) E E+T
2) E T
3) T T*F
4) T F
5) F id
6) F cte
7) F (E)
E(Expresión), T (Término), F (Factor)
Algoritmo de PA:
Hay que empezar marcando el Estado de Avance del Reconocimiento de una Regla (llamado Item), se
expresa con un punto.
Ej:
T .T*F No se ha reconocido nada de la regla.
T T.*F Se ha reconocido parcialmente.
T T*.F Se ha reconocido parcialmente.
T T*F. Se ha reconocido toda la regla.
a) El PA exige que la gramática tenga una sola definición. Si no se cumple esa condición se agrega una
regla nueva donde se cambia la Hipótesis y se redefine la Hipótesis en base a la regla vieja. Un ejemplo de
definición duplicada es el caso de E.
0) E´ E
1) E E+T
2) E T
3) T T*F
4) T F
5) F id
6) F cte
7) F (E)
- 35 -
b) Luego se construyen grupos de ítems:
Grupo Cero:
El primer grupo se construye poniendo el punto delante del lado derecho de las reglas.
Luego, si el punto quedó delante de un No Terminal se escribe su definición.
Para finalizar agregamos la palabra recursivamente.
1) E´ E.
E E . +T
2) E T.
T T . *F
Grupo Tres: [0, F], Luego de G9 [0,6 y F], G10 [0,6,7 y F], G11 [0,6,7,8 y F],
Si en el grupo 0 encuentro una F
3) T F.
- 36 -
Grupo Cuatro: [0, id], Luego de G9 [0,6 y id], G10 [0,6,7 y id], G11 [0,6,7,8 y id],
Si en el grupo 0 encuentro una id.
4) F id .
Grupo Cinco: [0, cte], Luego de G9 [0,6 y cte], G10 [0,6,7 y cte], G11 [0,6,7,8 y cte],
Si en el grupo 0 encuentro una cte.
5) F cte .
Grupo Seis: [0, (], Luego de G9 [0,6 y (], G10 [0,6,7 y (],G11 [0,6,7,8 y (],
Si en el grupo 0 encuentro un (.
- 37 -
Con el grupo 2, 3, 4 y 5 ya no puedo encontrar mas nada entonces empiezo con el 6.
9) F (E . )
E E . +T
Con el grupo 6 ya no puedo encontrar mas nada tal que, las reglas que se descubren luego ya lo fueron en
los grupos 2,3,4,5 y 6, ese dato se encola (Color ciruela).
10) E E +T .
T T . *F
Con el grupo 7 ya no puedo encontrar mas nada tal que, las reglas que se descubren luego ya lo fueron en
los grupos 3,4,5 y 6, ese dato se encola (Color anaranjado).
11) T T*F .
Con el grupo 8 ya no puedo encontrar mas nada tal que, las reglas que se descubren luego ya lo fueron en
los grupos 4,5 y 6, ese dato se encola (Color Fucsia).
12) F (E) .
El otro estado que aparece en el Grupo Nueve ya está definido en el Grupo Siete, entonces encola (color
gris).
La última regla que falta estudiar es la segunda del Grupo Diez cuyo estado ya está definido en el Grupo
Ocho, ese dato se encola (Color Violeta)
- 38 -
Segundo Paso del PA:
Construir una matriz de Terminales y NT como columnas y los grupos del paso 1 como filas. Una vez
construido esto lo veo como un autómata llamando estados a los grupos.
En la intersección de 0 y E escribo un 1
En la intersección de 0, 6 y T escribo un 2
En la intersección de 0, 6, 7 y F escribo un 3
En la intersección de 6 y E escribo un 9
En la intersección de 7 y T escribo un 10
En la intersección de 8 y F escribo un 11
En la intersección de Grupo desde y el No terminal, escribo el Número del Grupo al cual llego.
III ) Ahora voy a buscar todos los Grupos a los cuales llegué por medio de un Terminal y los voy a
identificar con una letra: (Paso los datos de los colas de T a la matriz)
En la intersección de 0, 6, 7, 8 y id escribo d4
En la intersección de 0, 6, 7, 8 y cte escribo d5
En la intersección de 0, 6, 7, 8 y ( escribo d6
En la intersección de 1, 9 y + escribo d7
En la intersección de 2, 10 y * escribo d8
En la intersección de 9 y ) escribo d12
En la intersección de Grupo desde y el Terminal, escribo el Número del Grupo al que llegué por medio de
la regla.
IV ) Ahora hay que mirar el conjunto de reglas que tienen el punto al final (ya definidas). El conjunto de
Reglas es 7 + 1 originalmente.
Si hubiera alguna duplicación voy a tener un conflicto (Reduce), del mismo modo si un grupo tuviera dos
reglas.
Si se produce alguna de ellas sucede que este algoritmo no puede compilar dicha gramática.
En el ejemplo puede verificarse que:
- 39 -
V ) Ahora debo hacer los reemplazos que mandan las reglas, los reemplazos válidos dependen de que el
carácter que sigue esté definido en las mismas 7 reglas.
Entonces vamos a construir los conjuntos de los posibles caracteres siguientes de los NT: E´, E, T y
F.
( E´) = {$}
( E ) = {+, ), $}
( T ) = {*, +, ), $}
( F ) = {*, +, ), $}
Id Cte * + ( ) $ E T F
0 D4 D5 D6 1 2 3
1 D7 FIN
2 D8 R2 R2 R2
3 R4 R4 R4 R4
4 R5 R5 R5 R5
5 R6 R6 R6 R6
6 D4 D5 D6 9 2 3
7 D4 D5 D6 10 3
8 D4 D5 D6 11
9 D7 D12
10 D8 R1 R1 R1
11 R3 R3 R3 R3
12 R7 R7 R7 R7
D: desplazamiento; R: Reducción; 1..11 son los grupos del PASO 1
Todo esto lo realiza automáticamente el YACC, manualmente, en la práctica para con los lenguajes reales
sería imposible. YACC produce esta matriz y construye un programa que hace funcionar el autómata.
- 40 -
Ejemplo de cómo ejecuta el compilador el Parsing Ascendente.
((id)) según la gramática antes descripta sería:
E
( ( id ) )
Ahora veremos como se van aplicando las reglas de la matriz: (( ID ))
- 41 -
3 E
2 T
9 F
3 T
2 F
9 E
3 T
2 F
1 FIN ((id))
- 42 -
Luego voy a repasar el parsing mirando solamente las reducciones:
5 * R6
3 * R4
4 + R5
3 + R4
2 + R2
5 ) R6
3 ) R4
10 ) R1
12 $ R7
11 $ R3
12 $ R2
6, 4, 5, 4, 2, 6, 4, 1, 7, 3, 2
COMPILADORES
Análisis Léxico En un compilador el análisis lineal se llama análisis léxico o exploración. Análisis lineal,
en la cadena de caracteres que constituye el programa fuente se lee de izquierda a derecha y se agrupa
en componentes léxicos, que son secuencias de caracteres que tienen un significado colectivo. Un
analizador léxico puede aislar un analizador sintáctico de la representación en lexemas de los
componentes léxicos. Se empieza haciendo una lista de algunas de las funciones para que las realice un
analizador léxico.
Análisis Sintáctico Este implica agrupar los componentes léxicos del programa fuente en frases
gramaticales que el compilador utiliza para sintetizar la salida. Por lo general, las frases gramaticales del
programa fuente se representan mediante un árbol de análisis sintáctico.
Árbol Sintáctico Es una representación compacta del árbol de análisis en el que los operadores aparecen
como los nodos interiores y los operandos de un operador son los hijos de nodo para ser operador.
Análisis Semántico La fase de análisis semántico revisa el programa fuente para tratar de encontrar
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 y proposiciones.
Tabla de Símbolos Es una estructura de datos que contiene un registro por cada identificador, con los
campos para los atributos del identificador. La estructura de datos permite encontrar rápidamente el
registro de cada identificador y almacenar o consultar rápidamente datos de ese registro.
- 43 -
Detección e Información de Errores Las fases de análisis sintáctico y semántico por lo general
manejan una gran porción de los errores detectables por el compilador. La fase léxica puede detectar
errores donde los caracteres restantes de la entrada no forman ningún componente léxico del lenguaje.
Los errores donde la cadena de componentes léxicos violan las reglas de estructura (sintaxis) del lenguaje
son determinados por la fase de análisis sintáctico. Durante el análisis semántico el compilador intenta
detectar construcciones que tengan la estructura sintáctica correcta, pero que no tengan significado para
la operación implicada, por ejemplo si se intenta sumar dos identificadores, uno de los cuales es el nombre
de una matriz, y el otro, el nombre de un procedimiento.
Generación de Código La fase final de un compilador es la generación de código objeto, que por lo
general consisten en código de máquina relocalizable o código ensamblador. Las posiciones de memoria se
selecciona para cada una de las variables usadas por el programa.
Código Ensamblador Es una versión mnemotécnica del código de máquina, donde se usan nombres en
lugar de códigos binarios para operaciones, y también se usan nombres para las direcciones de memoria.
Gramática Independiente del Contexto Una gramático describe de forma natural la estructura
jerárquica de muchas construcciones de los lenguajes de programación.
Una gramática independiente del contexto tiene 4 componentes:
1) Un conjunto de componentes léxicos, denominados símbolos terminales.
2) Un conjunto de no terminales.
3) Un conjunto de producciones, en el que cada producción consta de un no terminal, llamado lado
izquierdo de la producción, una flecha y una secuencia de componentes léxicos y no terminales, o
ambos, llamado lado derecho de la producción.
4) La denominación de uno de los no terminales como símbolo inicial.
Razones para dividir el análisis léxico y el análisis sintáctico Hay varias razones:
1) Un diseño sencillo es quizás la consideración más importante. Separa el análisis léxico del análisis
sintáctico a menudo permite simplificar una u otra de dichas fases. Por ejemplo, un analizador
sintñactico que incluya las convenciones de los comentarios y espacios en blanco es bastante más
complejo que uno que pueda comprobar si los comentarios y espacio en blanco ya han sido
eliminados de la convenciones léxicas de las sintácticas puede dar origen a un diseño del lenguaje.
- 44 -
2) Se mejora la eficiencia del compilador. Un analizador léxico independiente permite construir un
procesador especializado y potencialmente más eficiente para esta función. Gran parte de tiempo
se consume en leer el programa fuente y dividirlo en componentes léxicos. Con técnicas
especializadas de manejo de buffers para la lectura de caracteres de entrada y procesamiento de
componentes léxicos se puede mejorar significativamente el rendimiento de un compilador.
3) Se mejora la transportabilidad del compilador, las peculiaridades del alfabeto de entrada y otras
anomalías propias de los dispositivos pueden limitarse al analizador léxico. A representación de
símbolo especiales o no estándar, como en Pascal, pueden ser aisladas en el analizador léxico.
Autómatas Finitos Un reconocedor de un lenguaje es un programa que toma como entrada una cadena
‘x’ y responde “si” si ‘x’ es una frase del programa, y “no”, si no lo es. Se compila una expresión regular
en un reconocedor construyendo un diagrama de transición generalizado llamado autómata finito. Un
autómata finito puede ser determinista o no determinista, donde “no determinista” significa que en un
estado se puede dar el caso de tener más de una transición para el mismo símbolo de entrada.
Ventajas de la gramática.
1) Una gramática da una especificación sintáctica precisa y fácil de entender de un lenguaje de
programación.
2) A partir de algunas clases de gramáticas se puede construir automáticamente un analizar sintáctico
eficiente que determine si un programa fuente está sintácticamente bien formado. Otra ventaja es que
el proceso de construcción del analizador sintáctico puede revelar ambigüedades sintácticas y otras
construcciones difíciles de analizar que de otro modo podrían pasar sin detectar en la fase inicial de
diseño de un lenguaje y de su compilador.
3) Una gramática diseñada adecuadamente imparte una estructura a un lenguaje de programación útil
para la traducción de programas fuente a código objeto correcto y para la detección de errores.
Existen herramientas para convertir descripciones de traducciones basadas en gramática en programas
operativos.
Tratamientos de Errores Cuando se detecta luego de que se publica para el usuario. ¿Qué pasa luego
que se detecta el error?
1) Pánico, Resincronización. Provoca “ERROR”. Ignora el programa hasta un punto seguro (;) y sigue con
el resto. Basta que suceda uno para que siga compilando entre ‘;’, pero es para que el usuario sepa cual
es el error.
2) Borrado, Inserción y Reemplazo. Genera “WARNINGS”. No suspende la generación de código. Consiste
en tomar lo que hay en programa y cambiarlo. Genera Warnings y sigue generando código. Ej. Cambiar el
tamaño de una variable por una acortada. Borrado: Si viene una nueva línea lo cambia por otra cosa.
3) Gramática de Error. Genera “WARNINGS”. Se agrega un BNF un terminal que sea error y se escribe lo
que sería error (Se definen errores en BNF). Ej. <ERROR>id cte / + - / * +
Inconveniente: 1- La cantidad de errores es muy alta. 2- la ejecución se vuelve muy lenta.
Por lo tanto, solo se utiliza para errores clásicos. Ej. Cuando termina un programa en Pascal.
Clasificación de Errores Si se escribe ‘eles’ en vez de ‘else’ es un error léxico que el compilador no
puede detectar lo agarra el analizador sintáctico. Hay errores en las distintas fases.
1- Léxicos: Casi todos “WARNINGS”. Se los trata con Borrado, inserción y Reemplazo. Son errores
descubiertos en la parte léxica.
2- Sintáctico: son prácticamente ‘error’. La mayor parte son ‘Pánico’ y muy pocos son por ‘Gramática
de error’. Un error sintáctico suspende la generación de código.
3- Generación de código: Son la mayoría ‘Warnings’. Se tratan con Borrado, Inserción y Reemplazo. Son
conocidos con errores de la Tabla de Símbolos (variables no declaradas) o declaradas dos veces, o tipo
incompatibles. Algunos aparecen en la optimización (suma de constantes fuera del límite).
- 45 -
Qué hago cuando descubrí un error? Qué hace el compilador después de un error?
1. Pánico.
a. No se dónde estoy parado, hasta que no aparezca algo (generalmente: “;”). A partir de
eso, no se compila más.
b. No genero Código.
c. Se usa en Parsing (casi siempre). Pocos casos, con gramática de error.
COMPILADORES
Mensajes de
error
- 46 -
Las fases de un compilador
Programa fuente
Analizador léxico
Analizador sintáctico
Optimizador de código
Generador de código
Programa objeto
Analizador léxico
El analizador léxico es la primera fase de un compilador. Su principal función consiste en leer los
caracteres de entrada y elaborar como salida una secuencia de componentes léxicos que utiliza el
analizador sintáctico para hacer el análisis. Esta interacción suele aplicarse convirtiendo al analizador
léxico en una subrutina del analizador sintáctico. Recibida la orden “obtener el siguiente componente
léxico” del analizador sintáctico, el analizador léxico lee los caracteres de entrada hasta que pueda
identificar el siguiente componente léxico.
Cuando el analizador léxico es la parte del compilador que lee el texto fuente, también puede realizar
ciertas funciones secundarias en la interfaz del usuario, como eliminar del programa fuente comentarios y
espacios en blanco en forma de caracteres de espacio en blanco, caracteres TAB y de línea nueva. Otra
función es relacionar los mensajes de error del compilador con el programa fuente.
token
Programa Analizador Analizador
fuente léxico sintáctico
Obtén el siguiente token
Tabla de
símbolos
Aspectos del análisis léxico: Hay varias razones para dividir la fase de análisis de la compilación en
análisis léxico y análisis sintáctico.
1_ Un diseño sencillo es quizá la consideración más importante. Separar el análisis léxico del análisis
sintáctico a menudo permite simplificar una u otra de dichas fases.
2_ Se mejora la eficiencia del compilador.
3_ Se mejora la transportabilidad del compilador.
Componentes léxicos (token), patrones y lexemas: Cuando se menciona el análisis sintáctico, los
términos “componentes léxicos (token)”, “patrón”, “lexema” se emplean con significados específicos. En
general, hay un conjunto de cadenas en la entrada para el cual se produce como salida el mismo token.
- 47 -
Este conjunto de cadenas se describe mediante una regla llamada patrón asociado al token. Se dice que el
patrón concuerda con cada cadena del conjunto. Un lexema es una secuencia de caracteres en el
programa fuente con la que concuerda el patrón para un componente léxico.
Los componentes léxicos se tratan como símbolos terminales de la gramática del lenguaje fuente, con
nombres en negritas para representarlos. Los lexemas para el componente léxico que concuerdan con el
patrón representan cadenas de caracteres en el programa fuente que se pueden tratar juntos como una
unidad léxica.
En la mayoría de los lenguajes de programación, se consideran token las siguientes construcciones:
palabras clave, operadores, identificadores, constantes, cadenas literales y signos de puntuación,
paréntesis, punto y coma. Cuando en la secuencia de caracteres aparece pi en el programa fuente, se
devuelve al analizador sintáctico un componente léxico que representa un identificador. La devolución de
un componente léxico a menudo se realiza mediante el paso de un número entero correspondiente al
componente léxico. Este entero es al que hace referencia el id en negritas en la tabla.
Atributos de los componentes léxicos: Cuando concuerda con un lexema más de un patrón, el
analizador léxico debe proporcionar información adicional sobre el lexema concreto que concordó con las
siguientes fases del compilador. Por ejemplo, el patrón num concuerda con las cadenas 0 y 1, pero es
indispensable que el generador de código conozca que cadena fue realmente la que se emparejó.
El analizador léxico recoge información sobre los componentes léxicos en sus atributos asociados. Los
componentes léxicos influyen en las decisiones del análisis sintáctico, y los atributos, en la traducción de
los componentes léxicos. En la práctica, los componentes léxicos suelen tener un solo atributo –un
apuntador a la entrada de la tabla de símbolos donde se guarda la información sobre el componente
léxico; el apuntador se convierte en el atributo del componente léxico. A efectos de diagnóstico, puede
considerarse tanto el lexema para un identificador como el número de línea en el que éste se encontró por
primera vez. Estos dos elementos de información se pueden almacenar en la entrada de la tabla de
símbolos para el identificador.
Errores léxicos: Son pocos los errores que se pueden detectar simplemente en el nivel léxico porque un
analizador léxico tiene una visión muy restringida de un programa fuente. Si aparece la cadena fi por
primera vez en un programa en C en el contexto:
fi (a==f(x))...
un analizador léxico no puede distinguir si fi es un error de escritura de la palabra clave if o si es un
identificador de función no declarado. Como fi es un identificador válido, el analizador léxico debe devolver
el componente léxico de un identificador y dejar que alguna otra frase del compilador se ocupe de los
errores.
Pero supóngase que surge una situación en la que el analizador léxico no puede continuar porque ninguno
de los patrones concuerda con un prefijo de la entrada restante. Tal vez la estrategia de recuperación más
sencilla sea la recuperación en “modo de pánico”. Se borran caracteres sucesivos de la entrada restante
hasta que el analizador léxico pueda encontrar un componente léxico bien formado. Esta técnica de
recuperación puede confundir en ocasiones al analizador sintáctico, pero en un ambiente de computación
interactivo puede resultar bastante adecuada.
Otras posibles acciones de recuperación de errores son:
1. borrar un carácter extraño.
2. insertar un carácter que falta.
3. reemplazar un carácter incorrecto por otro correcto.
- 48 -
4. intercambiar dos caracteres adyacentes.
Eliminación de espacios en blanco y comentarios: Muchos lenguajes permiten que aparezcan
“espacios en blanco” (caracteres en blanco, caracteres TAB y de nueva línea) entre los componentes
léxicos. Los comentarios también pueden no ser considerados por el analizador sintáctico y el traductor,
por tanto también se pueden tratar como espacios en blanco.
Si el analizador léxico elimina los espacios en blanco, el analizador sintáctico nunca tendrá que
considerarlos. La alternativa de modificar la gramática para incorporar los espacios en blanco dentro de la
situación no es tan fácil de implementar.
Constantes: En cualquier momento que aparece un dígito solo en una expresión, parece razonable poner
una constante entera arbitraria en su lugar. Como una constante entera es una secuencia de dígitos,
pueden admitirse constantes enteras añadiendo producciones a la gramática de las expresiones o creando
token para tales constantes. La tarea de agrupar dígitos para formar enteros se le asigna, por lo general, a
un analizador léxico, porque los números se pueden tratar como unidades simples durante la traducción.
Sea num el token que representa un entero. Cuando una secuencia de dígitos aparece en la cadena de
entrada, el analizador léxico pasará num al analizador sintáctico. El valor del entero se pasará como
atributo del token num. Lógicamente, el analizador léxico pasa el token y el atributo al analizador
sintáctico. Al escribir un token y su atributo como una tupla encerrada entre < >, la entrada
31+28+59
el token + no tiene atributos. Los segundos componentes de las tuplas, los atributos, no desempeñan
papel alguno durante el análisis sintáctico, pero son necesarios en la traducción.
id = id + id;
- 49 -
Analizador sintáctico
El analizador sintáctico obtiene una cadena de componentes léxicos del analizador léxico, y comprueba
que la cadena pueda ser generada por la gramática del lenguaje fuente. Se supone que el analizador
sintáctico informará de cualquier error de sintaxis de manera inteligible. También debería recuperarse de
los errores que ocurren frecuentemente para poder continuar procesando el resto de su entrada.
token
Programa Analizador Analizador Resto de la
fuente léxico sintáctico Árbol de etapa inicial Representación
Obtén el análisis intermedia
siguiente token sintáctico
Tabla de
símbolos
Manejo de errores sintácticos: Si un compilador tuviera que procesar sólo programas correctos, su
diseño e implementación se simplificarían mucho. Pero los programadores a menudo escriben programas
incorrectos, y un buen compilador debería ayudar al programador a identificar y localizar errores.
Considerar desde el principio el manejo de errores puede simplificar la estructura de un compilador y
mejorar su respuesta a los errores.
Se sabe que los programas pueden contener errores de muy diverso tipo. Por ejemplo, los errores pueden
ser:
léxicos, como escribir mal un identificador, palabra clave u operador.
sintácticos, como una expresión aritmética con paréntesis no equilibrados.
semánticos, como un operador aplicado a un operando incompatible.
lógicos, como una llamada infinitamente recursiva.
Precedencia de operadores: Considere la expresión 9+5*2. Hay dos interpretaciones posibles de esta
expresión: (9+5)*2 o 9+(5*2).
Se dice que * tiene mayor precedencia que + si * considera sus operandos antes de que lo haga +. En
aritmética elemental, la multiplicación y división tienen mayor precedencia que la adición y sustracción.
Por tanto, 5 es considerado por * en 9+5*2 y en 9*5+2; es decir, las expresiones son equivalentes a
9+(5*2) y (9*5)+2, respectivamente.
Tabla de símbolos
Una tabla de símbolos es una estructura de datos que contiene un registro por cada identificador, con los
campos para los atributos del identificador (ámbito, enlace de los nombre, etc.). Se examina la tabla de
símbolos cada vez que se encuentra un nombre en el texto fuente. Si se descubre un nombre nuevo o
nueva información sobre un nombre ya existente, se produce cambios en la tabla. La estructura de datos
permite encontrar rápidamente el registro de cada identificador y almacenar o consultar rápidamente
datos de ese registro. Veamos un ejemplo de una traducción de una proposición.
- 50 -
posición :=inicial+velocidad*60
Analizador léxico
Analizador sintáctico
TABLA DE SÍMBOLOS
:=
1 posición *******
id1 +
2 inicial *******
id2 *
3 velocidad *******
id3 60
4
Generador de código
Representación de la información sobre el ámbito: Las entradas de la tabla de símbolos son para
declaraciones de nombres. Cuando se busca el caso de un nombre del texto fuente en la tabla de
símbolos, se debe devolver la entrada correspondiente a la declaración adecuada de dicho nombre. Las
reglas de ámbito del lenguaje fuente determinan qué declaración es la apropiada.
Un enfoque sencillo consiste en mantener una tabla de símbolos distinta para cada ámbito. En realidad, la
tabla de símbolos para un procedimiento o ámbito es el equivalente durante la compilación de un registro
de activación. Se encuentra la información para los nombres no locales de un procedimiento examinando
las tablas de símbolos correspondientes a los procedimientos abarcadores siguiendo las reglas de ámbito
del lenguaje. Asimismo, se puede asociar la información sobre los nombres locales de un procedimiento al
nodo correspondiente al procedimiento en un árbol sintáctico del programa. Con este enfoque la tabla de
símbolos se integra en la representación inmediata de la entrada.
Optimización de código
La fase de optimización de código trata de mejorar el código, de modo que resulte un código de máquina
más rápido de ejecutar. Algunas optimizaciones son triviales.
Generación de código
La fase final de un compilador es la generación de código objeto, que por lo general consiste en código
assembler.
Asignaciones de bits:
- 51 -
32 para enteros
32 para reales
64 para double
80 para BCD (18 dígitos + 2 exponente (10 99 o 10-99)
Instrucción Descripción
FLD dato Manda el contenido de una dirección al stack.
FST dato Copia de la pila a la memoria.
FSUB Resta el tope de la pila.
FADD Suma el tope de la pila.
FXCH dato Intercambia el contenido de la pila. Toma el
contenido de la primer dirección, lo pone en dato y lo
de dato lo manda a la primer dirección de la pila.
FSTP dato Copia el contenido de la primer dirección de la pila al
dato y borra ese dato de la pila.
FSUB dato El primer lugar de la pila menos un dato.
FADD dato El primer lugar de la pila más un dato.
FSUB ST (5) Al primer lugar, restarle lo que está en la quinta
posición de la pila.
FADD ST (5) Al primer lugar, sumarle lo que está en la quinta
posición de la pila.
FADD El primero más el segundo.
Instrucción Descripción
FILD Carga enteros de 32 bits.
FIST Almacenamiento de enteros de 32 bits.
FBLD
FBST Igual a las instrucciones descritas anteriormente,
FBSTP pero intercalando una “B”. La diferencia es que
FBADD manejan datos de 32 bits.
FBSUB
FDLD Opera con instrucciones de 64 bits (se intercala una
FDST D).
FDSTP Aclaración: el profesor no esta seguro si es una
FDADD D lo que se intercala.
FDSUB
Para todos los casos, FMUL (multiplicación) y FDIV (división). 16, 32 y 80 bits. No existe la noción de
acarreo, si la de overflow.
Ejemplo:
FLD ST (b) Toma lo del cuarto lugar de la pila, lo lleva al primer y todo baja de
lugar.
- 52 -
AA 0
CC 1
BB 2
XX 3
...
FLD ST (3)
XX 0
AA 1
BB 2
CC 3
...
Multiplicación
En cambio, la multiplicación, se hace siempre sobre AL si son 8 bits, sobre AX si son 16 bits y sobre
EDX si son 32 bits.
Resultados:
Ejemplos:
Instrucción bits registros donde queda
Multiplicación MUL CL 8 AX * CL AX
enteros sin
signo
MUL BX 16 AX * BX DX:AX
MUL ECX 32 EAX * ECX EDX:EAX
Multiplicación IMUL CL
enteros con
signo
IMUL BX
IMUL EBX
- 53 -
- 54 -