Você está na página 1de 11

4.

4 Tipos de Datos y Verificación de Tipos

Un compilador debe comprobar si el programa fuente sigue tanto las convenciones sintácticas
como las semánticas del lenguaje fuente. Esta comprobación, llamada comprobación estática
(para distinguirla de la comprobación dinámica que se realiza durante la ejecución del programa
objeto), garantiza la detección y comunicación de algunas clases de errores de programación. Los
ejemplos de comprobación estática incluyen:

Comprobaciones de tipos. Un compilador debe informar de un error si se aplica un operador a un


operando incompatible; por ejemplo, si se suman una variable tipo matriz y una variable de
función.

Comprobaciones del flujo del control. Las proposiciones que hacen que el flujo del control
abandone una construcción deben tener algún lugar a dónde transferir el flujo de control. Por
ejemplo, una proposición break en C hace que el control abandone la proposición que la engloba,
while, for o switch más cercana; si dicha proposición englobadora no existe, ocurre un error.

Comprobaciones de unicidad. Hay situaciones en que se debe definir un objeto una vez
exactamente. Por ejemplo, en Pascal, un identificador debe declararse de forma única, las
etiquetas en una proposición case deben ser diferentes y no se pueden repetir los elementos en
un tipo escalar.

Comprobaciones relacionadas con nombres. En ocasiones, el mismo nombre debe aparecer dos o
más veces. Por ejemplo, en Ada, un lazo o bloque puede tener un nombre que aparezca al
principio y al final de la construcción. El compilador debe comprobar que se utilice el mismo
nombre en ambos sitios.

Un comprobador de tipos se asegura de que el tipo de una construcción coincida con el previsto
en su contexto. Por ejemplo, el operador aritmético predefinido mod en Pascal exige operandos
de tipo entero, de modo que un comprobador de tipos debe asegurarse de que Los operandos de
mod tengan tipo entero. De igual manera, el comprobador de tipos debe asegurarse de que la
desreferenciación se aplique sólo a un apuntador, de que la indización se haga sólo sobre una
matriz, de que una función definida por el usuario se aplique al número y tipo correctos de
argumentos, etcétera.

Figura 4.15

Puede necesitarse la información sobre los tipos reunida por un comprobador de tipos cuando se
genera el código. Por ejemplo, los operadores aritméticos como + normalmente se aplican tanto a
enteros como a reales, tal vez a otros tipos, y se debe examinar el contexto de + para determinar
el sentido que se pretende dar. Se dice que un símbolo que puede representar diferentes
operaciones en diferentes contextos está "sobrecargado". La sobrecarga puede ir acompañada de
coacción de tipos, donde un compilador proporciona un operador para convertir un operando en
el tipo esperado por el contexto. Una noción diferente de la de sobrecarga es la de
"polimorfismo". El cuerpo de una función polimórfica puede ejecutarse con argumentos de varios
tipos.

SISTEMAS DE TIPOS

El diseño de un comprobador de tipos para un lenguaje se basa en información acerca de las


construcciones sintácticas del lenguaje, la noción de tipos y las reglas para asignar tipos a las
construcciones de lenguaje. Los siguientes extractos del informe de Pascal y del manual de
referencia de C, respectivamente, son ejemplos de la información con la que el diseñador de un
compilador podría verse obligado a comenzar.

"Si ambos operandos de los operadores aritméticos de suma, sustracción y multiplicación son de
tipo entero, entonces el resultado es de tipo entero."

"El resultado del operador unario & es un apuntador hacia el objeto al que se refiere el operando.
Si el tipo del operando es '...', el tipo del resultado es 'apuntador a .....´.”.

En los anteriores extractos se encuentra implícita la idea de que cada expresión tiene asociado un
tipo. Además, los tipos tienen estructura; el tipo "apuntador a ... " se construye a partir del tipo al
que se refiere " ... ".

En Pascal y en C, los tipos son básicos o construidos. Los tipos básicos son los tipos atómicos sin
estructura interna por lo que concierne al programador. En Pascal, los tipos básicos son boolean,
character, integer y real. Los tipos de subrango, como 1 .. 10, y los tipos enumerados, como

(violeta, índigo, azul, verde, amarillo, naranja, rojo)

se pueden considerar como tipos básicos. Pascal admite que un programador construya tipos a
partir de tipos básicos y otros tipos construidos, como matrices (array), registros (record) y
conjuntos (set). Además, los apuntadores y las funciones también pueden considerar~ como tipos
construidos.

Verificación de tipos

Un componente importante del análisis semántico es la verificación de tipos. aqui , el compilador


verifica si cada operador tiene operandos permitidos por la especificación del lenguaje fuente. Por
ejemplo, las definiciones de muchos lenguajes de programación requieren que el compilador
indique un error cada vez que se use un número real como indice de una matriz. Sin embargo, la
especificación del lenguaje puede permitir ciertas coerciones a los operandos, por ejemplo,
cuando un operador aritmético binario se aplica a un número entero y a un número real. En este
caso, el compilador puede necesitar convertir el número entero a real.

Expresiones de tipos

El tipo de una construcción de un lenguaje se denotará mediante una “expresión de tipo”. De


manera informal, una expresión de tipo es, o bien un tipo básico o se forma aplicando un operador
llamado constructor de tipos a otras expresiones de tipos. Los conjuntos de tipos y constructores
básicos dependen del lenguaje que deba comprobarse. Cada lenguaje de programación requerirá
unas expresiones de tipos adecuadas a sus características. A continuación, a modo de ejemplo, se
definen las expresiones de tipos más comunes:

Tipos simples: Son expresiones de tipos los tipos simples del lenguaje, y algunos tipos especiales:

Integer

Real

Char

Boolean

Void

Error

Los cuatro primeros son los tipos de datos simples más comunes en los lenguajes de
programación, los dos últimos son tipos simples especiales que usaremos para su atribución a
diversas partes de un programa, a fin de homogeneizar el tratamiento de todo el conjunto
mediante el método de las expresiones de tipos.

Tomando el lenguaje C como ejemplo, el segmento de código al que está asociada la expresión de
tipos integer es aquella en que aparece la palabra reservada int, etc.
Constructores de tipos: Permiten formar tipos complejos a partir de otros más simples. La
semántica de cada lenguaje tiene asociada unos constructores de tipos propios. En general, en los
lenguajes de programación se definen los siguientes constructores:

Matrices: Si T es una expresión de tipos, entonces array(R,T)es también una expresión de tipos que
representa a una matriz de rango R de elementos de tipo T.

Productos: Sea T1 y T2 expresiones de tipos, T1 x T2 es una expresión de tipos que representa al


producto cartesiano de los tipos T1 y T2. A fin de simplificar consideraremos que el constructor u
operador de tipos x es asociativo por la izquierda.

Registros: Sea un registro formado por los campos u1, u2 ... uN, siendo cada uno de ellos de los
tipos T1,T2 ... TN, entonces, la expresión de tipos asociada al conjunto es: record ( (u1:T1) x (u2:T2)
x ... x (uN:TN) )

Punteros: Si T es una expresión de tipos, entonces pointer(T) es una expresión de tipos que
representa a un puntero a una posición de memoria ocupada por un dato de tipo T.

Funciones: Sean T1,T2 ... TN, las expresiones de tipos asociadas a los segmentos de código
correspondientes a los argumentos de una función, y sea T el tipo devuelto por la función.
Entonces, la expresión de tipos asociada a la función es: ((T1xT2 x... xTN) -> T )

Las expresiones de tipo pueden contener variables cuyos valores son expresiones de tipos.

CONVERSIONES DE TIPOS, SOBRECARGA DE FUNCIONES Y OPERADORES, FUNCIONES


POLIMÓRFICAS

Conversiones de tipos

Considérense expresiones como x + i donde x es de tipo real e i es de tipo entero. Como


representación de enteros y reales es distinta dentro de un computador, y se utilizan instrucciones
de máquina distintas para las operaciones sobre enteros y reales, puede que el compilador tenga
que convertir primero uno de los operandos de + para garantizar que ambos operandos sean del
mismo tipo cuando tenga lugar la suma.

La definición del lenguaje especifica las conversiones necesarias. Cuando un entero se asigna a un
real, o viceversa, la conversión es al tipo del lado izquierdo de la asignación. En expresiones, la
transformación más común es la de convertir el entero en un número real y después realizar una
operación real con el par de operandos reales obtenidos. Se puede utilizar el comprobador de
tipos en un compilador para insertar estas operaciones de conversión en la representación
intermedia del programa fuente.
Coerciones

La definición del lenguaje especifica las conversiones necesarias. Cuando un entero se asigna a un
real, o viceversa, la conversión es al tipo del lado izquierdo de la asignación. En expresiones, la
transformación más común es la de convertir el entero en un número real y después realizar una
operación real con el par de operandos reales obtenidos. Se puede utilizar el comprobador de
tipos en un compilador para insertar estas operaciones de conversión en la representación
intermedia del programa fuente.

Sobrecarga de funciones y operadores

Un símbolo sobrecargado es el que tiene distintos significados dependiendo de su contexto. La


sobrecarga se resuelve cuando se determina un significado único para un caso de un símbolo
sobrecargado. La resolución de la sobrecarga a menudo aparece referida comoidentificación de
operadores porque determina la operación que denota un símbolo de operador.

Los operadores aritméticos están sobrecargados en la mayoría de los lenguajes. Sin embargo, la
sobrecarga que afecta a los operadores aritméticos como + se puede resolver observando
únicamente los argumentos del operador.

Funciones polimórficas

El término “polimórfico” se aplica a cualquier parte de código que pueda ejecutarse con
argumentos de tipos distintos, de modo que se puede hablar de funciones, así como de
operadores polimórficos.

Los operadores predefinidos para indicar matrices, aplicar funciones y manipular apuntadores son
generalmente polimórficos porque no se limitan a una determinada clase de matriz, función o
apuntador.

Expresiones y operadores
Expresiones.

Una expresión es una combinación de operadores y operandos de cuya evaluación


se obtiene un valor. Los operandos pueden ser nombres que denoten objetos
variables o constantes, funciones, literales de cualquier tipo adecuado de acuerdo
con los operadores u otras expresiones más simples. La evaluación de una expresión
da lugar a un valor de algún tipo, una expresión se dice que es del tipo de su
resultado. Ejemplos de expresiones:

a + 5*b
(a >= 0) and ((b+5) > 10)
a
-a * 2 + b
-b + sqrt(b**2 - 4*a*c)
length(s) > 0

Las expresiones se evalúan de acuerdo con la precedencia de los operadores. Ante


una secuencia de operadores de igual precedencia, la evaluación se realiza según el
orden de escritura, de izquierda a derecha. El orden de evaluación puede
modificarse usando paréntesis.

Operadores.

Ada agrupa los operadores en 6 categorías, de menor a mayor precedencia. Los


operadores binarios se usan en formato infijo (<operando_izquierdo> <operador>
<operando_derecho>), como en "a + b". Los operadores unarios se usan en formato
prefijo (<operador> <operando> ), como en "-5".

Operadores lógicos.

Están predefinidos para cualquier tipo, T, que designe un booleano, modular o


un array monodimensional de componentes booleanos:

function "and"(Left, Right : T) return T


function "or" (Left, Right : T) return T
function "xor"(Left, Right : T) return T

Ejemplo de uso en expresiones (sean A, B y C de tipo T):

if A and B then ...


C := A or B;
return (A xor B);

Su significado es el convencional (para los tipos modulares son operaciones bit a


bit):

A B (A and B) (A or B) (A xor B)
True True True True False
True False False True True
False True False True True
False False False False False

Para los tipos booleanos, existen versiones de los operadores "and" y "or",
llamadas "and then" y "or else" que tienen el mismo significado, pero realizan una
"evaluación en cortocircuito" consistente en que evalúan siempre primero el
operando izquierdo y, si el valor de éste es suficiente para determinar el resultado,
no evalúan el operando derecho.

La evaluación en cortocircuito resulta muy útil si la correcta evaluación del operando derecho
depende del valor del operando izquierdo, como en el siguiente ejemplo:
if i <= Vec'last and then Vec(i) > 0 then ...
(Evaluar Vec(i) produciría un error si i tiene un valor superior al límite máximo del vector).

Operadores relacionales.

Los operadores de igualdad están predefinidos para todos los tipos no limitados.
Sea T un tipo con estas características:

function "=" (Left, Right : T) return Boolean


function "/="(Left, Right : T) return Boolean

Los operadores de ordenación están predefinidos para todos los tipos escalares y los
arrays de elementos discretos. Sea T un tipo con estas características:

function "<" (Left, Right : T) return Boolean


function "<="(Left, Right : T) return Boolean
function ">" (Left, Right : T) return Boolean
function ">="(Left, Right : T) return Boolean

Existe también un operador de pertenencia ("in", "not in") que determina si un valor
pertenece a un rango o a un subtipo: if i in 1..10 then ...

Todos los operadores relacionales devuelven un resultado de tipo Boolean.

Ejemplo de uso en expresiones (sean A, B de tipo T y C de tipo Boolean):

if A = B then ...
C := (A <= B);
return (A >= B);

Operadores binarios de adición.


Los operadores de adición predefinidos para cualquier tipo numérico, T, son:

function "+"(Left, Right : T) return T


function "–"(Left, Right : T) return T

Ejemplo de uso en expresiones (sean A, B y C de tipo T):

C := A + B;
return (A - B);

También pertenecen a esta categoría los operadores de concatenación, predefinidos


para cualquier tipo de array monodimensional no limitado, T, de elementos de
tipo C:

function "&"(Left : T; Right : T) return T


function "&"(Left : T; Right : C) return T
function "&"(Left : C; Right : T) return T
function "&"(Left : C; Right : C) return T

Operadores unarios de adición.

Los operadores unarios de adición predefinidos para cualquier tipo númerico, T, son
la identidad y la negación:

function "+"(Right : T) return T


function "–"(Right : T) return T

Ejemplo de uso en expresiones (sean A y B de tipo T):

B := -A;

Cuando se aplica a un tipo modular, el operador de negación ("-") tiene el efecto de


restar el valor del operando, si es distinto de cero, al módulo. Si el valor del
operando es cero, el resultado es cero.

Operadores multiplicativos.

Los operadores de multiplicación y división están predefinidos entre diversas


combinaciones de enteros y reales:

function "*" (Left, Right : T) return T


function "/" (Left, Right : T) return T
function "*"(Left : T; Right : Integer) return T
function "*"(Left : Integer; Right : T) return T
function "/"(Left : T; Right : Integer) return T
function "*"(Left, Right : root_real) return root_real
function "/"(Left, Right : root_real) return root_real
function "*"(Left : root_real; Right :
root_integer) return root_real
function "*"(Left : root_integer; Right :
root_real) return root_real
function "/"(Left : root_real; Right :
root_integer) return root_real
function "*"(Left, Right :
universal_fixed) return universal_fixed
function "/"(Left, Right :
universal_fixed) return universal_fixed

Ejemplo de uso en expresiones (sean A, B y C del mismo tipo entero o real):

C := A * B;
return (A / B);

Los operadores módulo y resto están definidos para cualquier tipo entero, T:

function "mod"(Left, Right : T) return T


function "rem"(Left, Right : T) return T

La relación entre el resto y la división entera viene dada por la expresión: A =


(A/B)*B + (A rem B), donde (A rem B) tiene el mismo signo que A, y es menor
que B en valor absoluto.

El operador módulo se define de manera que (A mod B) tiene el mismo signo que
B, un valor absoluto menor, y existe un número entero, N, tal que: A = B*N +
(A mod B).

Ejemplo de uso en expresiones (sean A, B y C de tipo T):

C := A rem B;
return (A mod B);

Operadores de máxima prioridad.

Los operadores de máxima prioridad son: el operador de cálculo del valor absoluto,
definido para cualquier tipo numérico, T1, el operador de negación lógica, definido
para cualquier tipo booleano, modular o array monodimensional de componentes
booleanos, T 2, y el operador de exponenciación, definido para cualquier tipo
entero, T3, o para cualquier tipo real en coma flotante, T4. Cada uno, de acuerdo
con las siguientes especificaciones:
function "abs"(Right : T) return T
function "not"(Right : T) return T
function "**"(Left : T; Right : Natural) return T
function "**"(Left : T; Right : Integer'Base) return T

Ejemplo de uso en expresiones (sean A, B de tipo T1; C, D de tipo T2 y E de tipo


T3):

A := abs(B);
C := not D;
return (E ** 3); -- E elevado al cubo

Sobrecarga de operadores.

Ada permite que el programador sobrecargue los operadores del lenguaje, esto es,
que pueda redefinirlos dándoles nuevos significados. Para sobrecargar un operador,
simplemente hay que definir una función cuyo nombre sea el operador entre
comillas y que tenga los parámetros adecuados. Por ejemplo, dado el siguiente tipo:

type Complejo is record


PReal, PImag: float;
end record;

podemos sobrecargar el operador de suma ("+") para utilizarlo con el fin de sumar
números complejos:

function "+"(A, B : in Complejo) return Complejo is


Suma: Complejo;
begin
Suma.PReal := A.PReal + B.PReal;
Suma.PImag := A.PImag + B.PImag;
return Suma;
end "+";

Una vez definida esta función, se puede aplicar el operador entre variables del tipo
definido en los parámetros, devolviendo un valor del tipo definido como resultado.

...
S, X, Y: Complejo;
...
S := X + Y;

Sólo se pueden redefinir los operadores del lenguaje (no se pueden inventar
operadores) y manteniendo siempre su cardinalidad (los binarios se redefinirán con
funciones de dos parámetros y los unarios con funciones de un parámetro).
El operador de desigualdad ("/=") devuelve siempre el complementario de la igualdad ("="),
por lo que, en caso necesario, sólo hay que sobrecargar este último. De hecho, el operador de
desigualdad sólo se puede sobrecargar si se le atribuye un resultado que no sea de tipo
Boolean.

© Grupo de Estructuras de Datos y Lingüística Computacional - ULPGC.

Você também pode gostar