Você está na página 1de 33

CUADERNO DIDACTICO Nº

Borrador

ANALISIS SEMANTICO

EN PROCESADORES

DE LENGUAJE

(Borrador)

Juan Manuel Cueva Lovelle

Catedrático de E.U. de Lenguajes y Sistemas Informáticos

Departamento de Informática
Universidad de Oviedo

Noviembre-2000 (Borrador)
Análisis semántico en procesadores de lenguaje

1 INTRODUCCION
Se entiende por semántica como el conjunto de reglas que especifican el significado de cualquier sentencia

sintácticamente correcta y escrita en un determinado lenguaje.

En los apartados siguientes se estudiarán las distintas formas de especificar sintácticamente los lenguajes de progra-

mación y la forma de llevarlas a cabo dentro del procesador de lenguaje.

2 ESPECIFICACIONES SEMANTICAS DE LOS LENGUAJES DE PROGRAMACION


Existen dos formas para describir las especificaciones semánticas de un lenguaje de programación: especificación natural

y especificación formal.

La especificación natural de la semántica de un lenguaje de programación, se basa en utilizar el lenguaje natural para

especificar las características semánticas del lenguaje.

La especificación formal puede dividirse en dos grandes grupos:

- especificación por medio de gramáticas atribuidas o gramáticas con atributos.

- utilización de metalenguajes de especificación semántica.

2.1 Especificación natural o informal


La especificación natural de la semántica de un lenguaje de programación es describir mediante un lenguaje natural sus

características semánticas, que no sean deducibles de la gramática BNF que describe sintácticamente el lenguaje.

El analizador semántico deberá comprobar las especificaciones semánticas relatadas en lenguaje natural y las especi-

ficaciones semánticas derivadas de la sintaxis del lenguaje de programación.

2.1.1 Compatibilidad de tipos


La compatibilidad de tipos (type checking) es la principal especificación semántica derivada de la sintáxis del lenguaje.

2.1.2 Rutinas semánticas


Las especificaciones semánticas descritas en lenguaje natural, no tienen reglas fijas para ser implementadas en el

analizador semántico. Normalmente, lo que se hace es incorporar rutinas (procedimientos o funciones) denominadas

semánticas, que comprueban las especificaciones descritas en lenguaje natural. Dependiendo de la complejidad de la especi-

ficación, unas veces estas rutinas semánticas serán verdaderos procedimientos o funciones, y en otros casos serán simplemente

algunas sentencias intercaladas entre las específicas del análisis sintáctico.

2.2 Especificación formal


La especificación formal de los aspectos semánticos de los lenguajes de programación se basa en la definición

metamatemática, y no en la definición por medio del lenguaje natural. Es, por tanto, una especificación mucho más precisa, sin

embargo requiere el uso de unas herramientas que pueden dividirse en dos grandes grupos:

- Gramáticas atribuidas o con atributos.

- Lenguajes de especificación semántica.

-1-
Análisis semántico en procesadores de lenguaje

2.2.1 Gramáticas atribuidas o gramáticas con atributos


Las gramáticas con atributos o atribuidas, fueron definidas originalmente por Knuth (1968) para definir las especifi-

caciones semánticas de los lenguajes de programación. Una gramática atribuida se caracteriza por:

1.- La estructura sintáctica del lenguaje se define mediante una gramática libre de contexto.

2.- Cada símbolo de la gramática, tiene asociado un conjunto finito de atributos. Ejemplo:

<SIMBOLO>.a.b.c

siendo a, b y c atributos.

Cada atributo puede tomar un conjunto de valores (posiblemente infinito). El valor de un atributo describe una propiedad

dependiente del contexto del símbolo en cuestión

3.- Cada regla de producción debe de especificar como se modifican los atributos con su aplicación.

4.- Una gramática atribuida describe un sublenguaje (del lenguaje definido) mediante las condiciones de contexto que

deben ser cumplidas por los valores de los atributos. Una sentencia sintácticamente correcta también lo será

semánticamente si y sólo si todos los atributos satisfacen las condiciones del contexto.

El principal problema del proceso de gramáticas atribuidas, es que el tiempo necesario para realizar las comprobaciones

semánticas crece exponencialmente con el número de atributos.

2.2.2 Lenguajes de especificación semántica


Los lenguajes de especificación semántica son un caso particular de los métodos formales para desarrollo de software

[FRAS94].

Los lenguajes de especificación semántica se pueden dividir en dos grandes grupos orientados a definir un modelo y

orientados a definir propiedades.

2.2.2.1 Lenguajes orientados a definir un modelo


Están construidos con independencia de las primitivas, ya sean estas concretas o abstractas.

Las ventajas de usar lenguajes orientados a definir un modelo, estriban en que la existencia de un modelo asegura la

consistencia de la especificación, y que el modelo puede generar sugerencias al implementador. Por otra parte, la existencia

de un modelo incrementa la probabilidad de que un sistema sea implementable en un sistema físico concreto.

Las desventajas son por un lado que el uso de un modelo restringe y limita al implementador y, por otro, que puede

admitir más información de la estrictamente necesaria para describir las propiedades de la abstracción que se está considerando.

El primero de los lenguajes orientados a definir un modelo es el VDM (Vienna Develpment Method) usado para describir

lenguajes mediante semántica denotacional.

Posteriormente se desarrolló el HDM ( " Hierarchical Development Method ") basado en la especificación de programas

para distintos niveles de abstracción de una máquina dada [LEVI79].

-2-
Análisis semántico en procesadores de lenguaje

2.2.2.2 Lenguajes orientados a definir propiedades


En este tipo de lenguajes, las especificaciones se indican en términos de axiomas que definen las relaciones entre cada

operación, no definiendo, por tanto, ni tipos de datos ni valores. El ejemplo más conocido es el método algebraico de espe-

cificación de tipos abstractos de datos.

Las ventajas del método algebraico son:

- No influye ni restringe al implementador.

- Facilita la comprobación de la correcta descripción de las propiedades.

Los inconvenientes del método algebraico son:

- Menor facilidad de construcción.

- Dificultad para expresar el comportamiento de un error.

- Dificultad para expresar la consistencia de una especifiación.

Los lenguajes utilizados son PROLOG, IOTA y ANNA (ANNotated Ada)

2.2.2.3 Metodologías de desarrollo de software


Los lenguajes anteriormente mencionados pueden incorporar herramientas auxiliares y metodologías de apoyo al

análisis, diseño y desarrollo de software. En estos casos, el lenguaje de especificaciones semánticas, también se utiliza como

lenguaje de programación para el desarrollo de software. Ejemplos: VDL, VDM, ANNA, GYPSY, HDM, ...

2.2.2.3.1 VDL (Vienna Definition Language)


Una de las primeras experiencias para desarrollar una especificación formal completa (léxica, sintáctica y semántica)

de un lenguaje de programación fue desarrollada en los laboratorios de Viena de IBM con la especificación formal del lenguaje

PL/I (Lucas y Walk 1969). Se utilizó la notación denominada VDL (Vienna Definition Language).

2.2.2.3.2 VDM (Vienna Development Method)


Se comenzó a desarrollar en los laboratorios IBM de Viena en 1970, de ahí su nombre, es una variante de

la semántica denotacional. Es una metodología de desarrollo de software con fuerte base matemática y formal.
VDM no está relacionado con VDL, pese a la conexión de Viena. Se caracteriza por:
- Construye métodos formales abstractos de las especificaciones.

- Emplea una notación formal matemática basada en los principios de la semántica denotacional.

- Cualquier procedimiento se escribe utilizando tipos abstractos de datos, y con rigurosas especificaciones.

- Cualquier sistema a desarrollar en VDM se divide en tres partes:

- entradas

- un estado que puede cambiar en respuesta a las entradas

- salidas

-3-
Análisis semántico en procesadores de lenguaje

- El modelo formal de cualquier sistema, consta de tres apartados:

1. Dominios sintácticos, son abstracciones de las entradas.

2. Dominios semánticos, corresponden a las abstracciones del estado.

3. Funciones semánticas, son las abstracciones de los cambios de estado y de las posibles salidas.

Se ha utilizado en desarrollo de bases de datos, definición de la semántica de lenguajes y para el desarrollo de compi-

ladores (PL/I, Ada,...).

2.2.2.3.3 HDM (Hierarchical Development Method)


Se desarrolló por SRI International como una herramienta de ayuda al desarrollo y mantenimiento del software.

- Consta de:

a) Un conjunto de reglas que describen las fases de desarrollo del software.

b) Un conjunto de lenguajes.

c) Un conjunto de herramientas auxiliares.

- La mayor parte de la notación de HDM es el lenguaje de especificaciones denominado SPECIAL, el cual se utiliza

para describir los módulos de un diseño.

- SPECIAL es una notación formal que permite definir los tipos de los argumentos y de los valores devueltos por un

procedimiento, así como el tipo de operación realizada por el mismo.

- Los procedimientos se agrupan en módulos, y los módulos en niveles de jerarquía de diseño.

- El lenguaje SPECIAL permite la ocultación de la información (information hiding) entre distintos niveles.

- HDM requiere el desglose en una jerarquía de niveles del sistema a desarrollar.

- Cada nivel se hace corresponder con una máquina abstracta.

- Cada máquina abstracta está descompuesta en un conjunto de módulos especificados en SPECIAL.

- Las facilidades disponibles en cada nivel están implementadas utilizando las facilidades del nivel inmediato inferior.

- El nivel más bajo o inferior es el que soporta las primitivas que se encuentran implementadas en el propio sistema

físico.

- El nivel más alto es el interfaz con el usuario.

2.2.2.3.4 ANNA (ANNotated Ada)


Es una extensión del lenguaje Ada, que incluye facilidades para la especificación formal de programas en todas las

fases de desarrollo.

- Un programa ANNA es básicamente un programa Ada con comentarios formales.

- Los comentarios consisten en:

- texto virtual Ada

- anotaciones

-4-
Análisis semántico en procesadores de lenguaje

- Las anotaciones pueden ser:

- anotaciones declarativas: para variables, subtipos, subprogramas, paquetes,...

- anotaciones de sentencia

- anotaciones de excepción

- anotaciones de visibilidad

- ANNA incluye un pequeño número de atributos predefinidos, los cuales sólo pueden aparecer en las anotaciones.

- La estructura léxica de ANNA está diseñada de forma que las extensiones de Ada aparezcan como comentarios siendo

aceptables por cualquier compilador de Ada.

-- esto es un comentario en Ada

- La semántica de las anotaciones está definida en términos de conceptos de Ada. De hecho, muchas anotaciones son

generalizadas de conceptos restringidos. Ello simplifica y facilita a los programadores de Ada la especificación

formal de sus programas mediante el uso de ANNA.

- El sistema ANNA intenta proporcionar una estructura en la cual diferentes teorías de especificación formal puedan

ser aplicadas al lenguaje Ada.

- El sistema ANNA, como lenguaje de especificación formal, se puede aplicar a otros lenguajes diferentes del Ada

(Pascal, PL/I, Modula,...) aunque sólo se dispone de subconjuntos de ANNA.

- Las herramientas de soporte que incluye el sistema ANNA son:

- Analizador sintáctico, editor estructurado y detector de manejador de errores y de inconsistencias entre

especificaciones y código.

- Sistema de comprobación en tiempo de ejecución el cual traduce las especificaciones formales en rutinas

de chequeo Ada.

- Sistema de verificación formal.

2.2.2.3.5 GYPSY
Se comenzó a desarrollar en 1974 en la Universidad de Texas.

Incluye:

- metodología

- notación (tipo Pascal)

- lenguaje

- herramientas auxiliares, para la especificación y verificación formal.

- El GYPSY se puede usar tanto como lenguaje de especificación como lenguaje de programación.

- Permite el desarrollo modular de programas.

- Facilita las tareas de validación.

-5-
Análisis semántico en procesadores de lenguaje

- La notación es del tipo Pascal.

- Permite la concurrencia (COBEGIN-COEND).

- Permite el manejo de excepciones o condiciones predefinidas o definidas por el usuario.

3 GRAMATICAS CON ATRIBUTOS


Una gramática con atributos (o atribuida) es una gramática de contexto libre a cuyos símbolos X (terminales y no

terminales) se les asocia un conjunto de atributos. Además a cada regla sintáctica se le asocian unas reglas semánticas que

especifican como los atributos toman sus valores.

3.1 Atributos
Un atributo es una variable que representa una determinada propiedad del símbolo X y que puede tomar un valor

cualquiera dentro de un conjunto de valores posibles. Los atributos se denotan con un nombre precedido por un punto y el

nombre del símbolo al que está asociado. Por ejemplo X.tipo, X.valor, etc... Al conjunto de atributos asociados al símbolo X,

se denotará por A(X).

El atributo habitual en los compiladores es el tipo, sin embargo en los intérpretes puros se pueden tener dos atributos:

el tipo y el valor.

3.1.1 Ejemplo de atributos


Sea una pequeña gramática libre de contexto para expecificar expresiones aritméticas y asignaciones de un intérprete.

(1) <ASIGNACION> → <VARIABLE> = <EXPRESION>

(2) <EXPRESION> → <EXPRESION> <OPERADOR> <EXPRESION>

(3) <EXPRESION> → número

(4) <VARIABLE> → identificador

(5) <OPERADOR> → +

(6) <OPERADOR> → -

Ejemplos de sentencias de este lenguaje son: c = 1+1, velocidad = 3.2 + 4 - 5.0, tocino = 4 - 5.1.

Se pueden definir los siguientes atributos:

número.tipo Tipo del número (entero o real)

número.valor Valor que contiene el token número.

<VARIABLE>.tipo Tipo de la variable

<VARIABLE>.valor Valor que toma la variable

<EXPRESION>.tipo Tipo de la expresión

<EXPRESION>.valor Valor de la expresión después de evaluarla

<OPERADOR>.tipo Tipo del operador (entero o real)

-6-
Análisis semántico en procesadores de lenguaje

<OPERADOR>.clase Clase del operador (adición y sustracción)

El símbolo terminal identificador no tiene el atributo tipo en este ejemplo dado que no existe declaración obligatoria

de variables en esta gramática. En el caso de una gramática con declaraciones obligatorias, en la zona de declaraciones es donde

los identificadores reciben el atributo tipo.

3.2 Reglas semánticas


Los valores que toman los atributos se establecen en función de un conjunto de reglas semánticas que están asociadas

a cada una de las reglas sintácticas. Al conjunto de reglas semánticas asociado al conjunto de reglas sintácticas P se denominará

R(P). Las reglas semánticas se definen en función de los atributos de los demás símbolos que componen la regla y otras posibles

acciones. Dado que los símbolos de una gramática pueden definirse recursivamente y repetidamente en las reglas sintácticas

de la gramática, las reglas semánticas los distinguen por medio del uso de subíndices numerados de izquierda a derecha de las

reglas. En las reglas también pueden incluirse otras acciones como cambiar de tipo, imprimir, etc... que se denominarán acciones

semánticas.

Además de las reglas semánticas existe un conjunto de condiciones semánticas que se ejecutan sobre los atributos y

que se denominarán B(P).

Cada símbolo de la gramática corresponde a un nodo del árbol sintáctico de una sentencia del lenguaje generado por

la gramática. Los símbolos terminales se corresponden con las hojas del árbol. En una gramática con atributos los nodos y las

hojas del árbol también contienen los atributos, que se evaluarán teniendo en cuenta las reglas semánticas.

3.2.1 Ejemplo de reglas y condiciones semánticas


Continuando con la gramática del ejemplo 3.1.1, se puede ampliar la gramática dada con las siguientes acciones y reglas

semánticas para construir un intérprete puro de dicha gramática.

(1) <ASIGNACION> → <VARIABLE> = <EXPRESION>

{<VARIABLE>.valor := <EXPRESION>.valor
<VARIABLE>.tipo := <EXPRESION>.tipo}

(2) <EXPRESION> → <EXPRESION> <OPERADOR> <EXPRESION>

{ <OPERADOR>.tipo := mayor_tipo(<EXPRESION>2.tipo, <EXPRESION>3.tipo)

<EXPRESION>1.tipo := <OPERADOR>.tipo

if (<OPERADOR>.tipo==’F’ && <EXPRESION>2.tipo==’I’)

{<EXPRESION>2.tipo:=’F’;
<EXPRESION>2.valor:=FLOAT(<EXPRESION>2.valor);}

if (<OPERADOR>.tipo==’F’ && <EXPRESION>3.tipo==’I’)


{<EXPRESION>3.tipo:=’F’;

-7-
Análisis semántico en procesadores de lenguaje

<EXPRESION>3.valor:=FLOAT(<EXPRESION>3.valor);}

switch (<OPERADOR>.tipo)

{
’I’: <EXPRESION>1.valor :=op_entera(<OPERADOR>.clase, <EXPRESION>2.valor, <EX-

PRESION>3.valor); break;

’F’: <EXPRESION>1.valor :=op_real(<OPERADOR>.clase, <EXPRESION>2.valor,


<EXPRESION>3.valor); break;

(3) <EXPRESION> → número

{ <EXPRESION>.valor := número.valor
<EXPRESION>.tipo := número.tipo

(4) <VARIABLE> → identificador

(5) <OPERADOR> → + {<OPERADOR>.clase:=’+’}

(6) <OPERADOR> → - {<OPERADOR>.clase:=’-’}

Las funciones auxiliares utilizadas en las reglas semánticas se definen a continuación.


tipo Mayor_Tipo (char tipo1, char tipo2)

{
if (tipo1==tipo2) return tipo1;

if (tipo1==’F’ || tipo2==’F’) return ’F’;

else return ’I’;


}

int op_entera(char op, int valor1, int valor2)

{
switch (op)

’+’: return (valor1 + valor2);


’-’: return (valor1 - valor2);

float op_real(char op, float valor1, float valor2)


{

switch (op)

-8-
Análisis semántico en procesadores de lenguaje

’+’: return (valor1 + valor2);


’-’: return (valor1 - valor2);

Las función FLOAT convierte un entero en real.

3.2.2 Ejemplo de evaluación de atributos mediante reglas semánticas


Se utilizarán las reglas semánticas del ejemplo 3.2.1 para evaluar los atributos de la sentencia:
velocidad = 80.5 + 20

Se construye el árbol sintáctico con los atributos que se muestra en la figura 1.

ASIGNACION

VARIABLE = EXPRESION 1
valor tipo valor tipo

identificador EXPRESION 2 OPERADOR EXPRESION3


valor tipo tipo clase valor tipo

velocidad
número + número

valor tipo valor tipo

80.5 20

Figura 1. Arbol sintáctico con atributos

El analizador léxico devuelve los tokens reconocidos con sus atributos:


velocidad.identificador = 80.5.float + 20.int

A partir de esta información los atributos se propagan según las reglas y acciones semánticas definidas en el apartado

3.2.1, obteniéndose:

<EXPRESION>2.valor:=80.5

<EXPRESION>2.tipo:=’F’

<EXPRESION>3.valor:=20

<EXPRESION>3.tipo:=’I’

<OPERADOR>.clase:=’+’

<OPERADOR>.tipo:=’F’

<EXPRESION>1.tipo:=’F’

-9-
Análisis semántico en procesadores de lenguaje

<EXPRESION>3.tipo:=’F’

<EXPRESION>3.valor:=20.0

<EXPRESION>1.valor:=100.5

<VARIABLE>.tipo:=’F’

<VARIABLE>.valor:=100.5

3.3 Atributos heredados


Se dice que un atributo en una determinada posición del árbol es heredado si se calcula mediante los atributos del padre

o de los hermanos de dicho nodo. Al conjunto de atributos heredados se denominará AH(x).

3.3.1 Ejemplos de atributos heredados


Continuando con el ejemplo 3.2.2 son atributos heredados:

<VARIABLE>.valor

<VARIABLE>.tipo

<OPERADOR>.tipo

3.4 Atributos sintetizados


Los atributos sintetizados se calculan a partir de los atributos de los nodos hijos del árbol. Al conjunto de atributos

sintetizados se denominará AS(x). Los símbolos terminales tan sólo tienen atributos sintetizados, y es el analizador léxico quien

se encarga de proporcionar los atributos. Además se supone que el símbolo inicial no tiene ningún atributo heredado.

3.4.1 Ejemplos de atributos sintetizados


Continuando con el ejemplo 3.2.2 son atributos sintetizados:

<OPERADOR>.clase

<EXPRESION>.valor

<EXPRESION>.tipo

número.tipo

número.valor

3.5 Definición formal de gramáticas con atributos


Una gramática con atributos o atribuida es una cuadrupla formada por GA={G, A, R, B}, donde:

G={VN,VT,S,P} es una gramática libre de contexto

A= U A(X) es el conjunto de atributos asociados a cada símbolo X de la gramática (terminales y

no terminales). Un símbolo no puede tener el mismo atributo repetido, es decir los

atributos de cada símbolo deben ser conjuntos disjuntos:

Si A(X) ∩ A(Y) <> ∅ ⇒ X = Y

- 10 -
Análisis semántico en procesadores de lenguaje

R=U R(P) es un conjunto finito de reglas semánticas asociadas a cada una de las reglas P de la

gramática libre de contexto G. Si entre las reglas semánticas hay acciones la gramática

atribuida pasa a denominarse Definición atribuida o definición dirigida por sintaxis.

Sea la regla p de la forma:

X0 → X1X2X3...Xn

entonces las reglas semánticas asociadas a p son de la forma:

a:=f(a1,a2,a3,...,an)

donde f es una función y a,ai son atributos de los símbolos de la regla que componen

p.

B=U B(P) es un conjunto de condiciones asociado a cada una de las reglas P de la gramática libre

de contexto G.

Sea la regla p de la forma:

X0 → X1X2X3...Xn

entonces las condiciones semánticas asociadas a p son de la forma:

g(a1,a2,a3,...,an)

donde g es una función booleana y a,ai son atributos de los símbolos de la regla que

componen p.

3.5.1 Definición formal de atributos calculados

Sea la regla p de la forma: X0 → X1X2X3...Xn se define AC(p) como el conjunto de atributos calculados mediante la

regla p:

AC(p)={X1.a / X1.a=f(...) pertenece a R(p)}

3.5.2 Definición formal de atributos sintetizados


Dada una gramática con atributos, se dice que un atributo a asociado a un símbolo X es sintetizado si existe una regla

sintáctica de la forma X → α y una regla semántica que calcula a con los atributos de los símbolos de la parte derecha de dicha
regla. Al conjunto de atributos sintetizados se denominará AS(x).

AS(X)={X.a/Existe X → α perteneciente a P y X.a pertenece a AC(p)}

3.5.3 Definición formal de atributos heredados


Dada una gramática con atributos, se dice que un atributo a asociado a un símbolo X es heredado si existe una regla

sintáctica de la forma Y → αXβ y una regla semántica que calcula a con los atributos del resto de los símbolos que forman la

regla. Al conjunto de atributos heredados se denominará AH(x).

AH(X)={X.a/Existe Y → αXβ perteneciente a P y X.a pertenece a AC(p)}

- 11 -
Análisis semántico en procesadores de lenguaje

3.5.4 Gramática con atributos completa


Se dice que una gramática con atributos es completa si cumple las cuatro condiciones siguientes para todo símbolo X.

• Para toda regla p: X → α AS(X) está incluido en AC(p)

• Para toda regla q: Y → αXβ AH(X) está incluido en AC(q)

• AS(X) U AH(X) = A(X)

• AS(X) ∩ AH(X) = ∅

3.5.5 Gramáticas con atributos bien definidas


Una gramática con atributos está bien definida si para toda sentencia L(G) todos los atributos pueden ser calculados.

A este proceso se denomina evaluación de la gramática.

Una sentencia del lenguaje se dice que tiene todos sus atributos correctos si además cumplen todas las condiciones B(p)

asociadas a las reglas se verifican.

3.5.6 Teorema de las gramáticas bien definidas


Toda gramática con atributos bien definida es completa.

3.6 Gramáticas S-atribuidas


Se dice que una gramática S-atribuida si sólo contiene atributos sintetizados.

3.6.1 Evaluación ascendente de gramáticas S-atribuidas


Toda gramática S-atribuida puede ser evaluada mediante análisis ascendente. Es necesario ampliar la pila del analizador

de forma que se almacenan juntos los símbolos y los atributos. Dado que tan sólo se utilizan los símbolos de la parte derecha

de cada producción con lo que se pueden realizare los calculos simultáneamente a las reducciones que se efectuan en la pila.

3.7 Gramáticas L-atribuidas


Se dice que una gramática es L-atribuida si todo atributo heredado asociado a un símbolo Xj aparece en la parte derecha

de alguna regla de la gramática A → X1X2X3...Xn depende solamente de:

• los atributos (sintetizados o heredados) de los símbolos de la parte derecha de la producción situados a su

derecha (X1X2X3...Xj-1)

• los atributos heredados de A

Puede observarse que la definición impone restricciones a los atributos heredados de Xj, pero no hay restricciones para

los atributos sintetizados. Por tanto, toda gramática S-atribuida es también L-atribuida.

3.7.1 Evaluación descendente de gramáticas L-atribuidas


Se pueden evaluar grmáticas L-atribuidas de forma simultánea al análisis LL(1). Cada función asociada a cada símbolo

no terminal tendrán como argumentos los atributos heredados correspondientes al símbolo no terminal, y devolveran los atributos

sintetizados asociados al símbolo no terminal.

- 12 -
Análisis semántico en procesadores de lenguaje

3.8 Esquemas de traducción atribuidos


Un esquema de traducción atribuido es una gramática atribuida en la cual las reglas semánticas aparecen intercaladas

entre los símbolos de la parte derecha de la producción, indicando así el momento que deben ser ejecutadas. Es decir son de la

forma: A → {R0} X1 {R1} X2 {R2} X3...Xn {Rn} donde Ri representa una o varias acciones semánticas.

3.8.1 Esquemas de traducción atribuidos bien definidos


Para que un esquema de traducción atribuido esté bien definido debe cumplir las siguientes condiciones: (condiciones

siguientes)

• Un atributo heredado asociado a un símbolo en la parte derecha de una regla, debe ser evaluado antes de que

se emplee la producción correspondiente a dicho atributo.

• Una regla semántica asociada a una producción sintáctica no debe hacer referencia a los atributos sintetizados

asociados a la parte izquierda de dicha producción.

• Un atributo sintetizado asociado a un símbolo terminal situado en la parte izquierda de una regla debe ser

evaluado después de que hayan sido evaluados todos los atributos de los que el dependa.

Con estas tres condiciones se garantiza que es posible realizar una correcta evaluación del esquema de traducción, y

por tanto que la gramática atribuida subyacente está bien definida.

También puede demostrarse que a partir de toda gramática L-atribuida es posible obtener un esquema de traducción

bien definido.

Un esquema de traducción bien definido asociado a una gramática S-atribuida tiene solamente reglas de la forma:

A → X1 X2 X3...Xn {Rn} Rn:={A.sk :=f(Xi.si)}

3.9 Generador de compiladores basado en gramáticas atribuidas


El desarrollo del sistema GAG se realizó intentando satisfacer tres premisas:

- Construcción de un sistema útil y práctico para la generación de compiladores.

- Construcción de un sistema capaz de procesar definiciones formales de lenguajes por medio de gramáticas atribuidas,

en vez de especificaciones en lenguaje natural.


- Construir evaluadores de atributos eficientes.

El lenguaje de entrada al GAG, se llama ALADIN (A Language for Attributed DefINition) y está basado en principios

de gramáticas atribuidas. La salida del generador es el código del compilador escrito en Pascal.

Gramática
Atribuida
del GAG compilador (en Pascal)
lenguaje
(en ALADIN)

Figura 2.

- 13 -
Análisis semántico en procesadores de lenguaje

El sistema GAG ha conseguido resultados satisfactorios incluso con lenguajes de alta complejidad como el ADA, con

buenas prestaciones en cuanto a las necesidades de memoria y el tiempo de ejecución.

La evaluación de los atributos, se realiza por medio de un autómata de pila, y se hace partiendo del árbol sintáctico

construido tras el análisis del texto fuente. Cada nodo del árbol representa a un descendiente del símbolo inicial tras aplicarle

las distintas reglas de producción. Las reglas semánticas se evalúan al pasar por cada nodo. Una secuencia del análisis semántico

sería:

- Evaluar una regla semántica sobre los atributos.

- Comprobar las condiciones de contexto.

- Pasar al siguiente nodo hijo (descendiendo).

- Liberar al nodo padre.

La siguiente fase es la traducción a Pascal de las reglas introducidas en ALADIN. Muchas de las expresiones en

ALADIN deben de descomponerse en varias sentencias de Pascal, utilizándose a veces variables temporales para los resultados

intermedios. Algunos operadores de ALADIN deben de ser traducidos como llamadas a procedimientos y funciones del lenguaje

Pascal.

Por otra parte el código emitido en Pascal debe de cumplir:

- No se deben violar las normas de tipos en Pascal.

- Legibilidad. Debe de formatearse y usarse identificadores lo más parecidos posibles a los usados con ALADIN.

La salida del GAG incluye además del código generado:

- mensajes de error;

- traza;

- tabla de referencias cruzadas;

- listado de las vías de dependencia entre los atributos seleccionados.

Los principios de la implementación del sistema GAG han sido:

a) MODULARIDAD. Cada fase está compuesta por módulos.

b) TRANSPORTABILIDAD. Se ha desarrollado en Pascal y genera código también en Pascal.

c) TOLERANCIA A FALLOS. Contiene una verificación de todas las conexiones entre módulos y un sistema de

tratamiento de errores en todas las fases del sistema.

- 14 -
Análisis semántico en procesadores de lenguaje

4 COMPATIBILIDAD DE TIPOS
4.1 Introducción
Un compilador debe verificar que el programa fuente sigue las convenciones sintácticas y semánticas del lenguaje.

Esta verificación, denominada COMPROBACION ESTATICA (para distinguirla de la comprobación dinámica que se rea-

lizaría durante la ejecución del programa objeto), asegura que ciertos tipos de errores pueden ser detectados e indicados al

programador. Ejemplos de comprobaciones estáticas son:

a) COMPATIBILIDAD DE TIPOS.

Un compilador debe de indicar un error si un operador se aplica a un operando incompatible, por ejemplo, si se desea

sumar una variable de tipo vector con un identificador de función.

b) COMPROBACION DEL CONTROL DE FLUJO.

Las sentencias que producen la variación del flujo de un programa deben verificar hacia donde se transfiere el control

de flujo. Por ejemplo, la sentencia break del lenguaje C hace que el control de flujo abandone el bucle while más interior o una

sentencia switch; aparecerá un error se tal bucle no existe.

c) COMPROBACION DE LA UNICIDAD.

Existen situaciones en las cuales un objeto sólo se define una vez. Por ejemplo, en Pascal un identificador debe de

declararse una sola vez, las etiquetas de la sentencia case deben de ser distintas, y los elementos de un tipo enumerado no deben

de repetirse.

d) COMPROBACION DE IDENTIFICADORES CORRESPONDIENTES.

Algunas veces, el mismo identificador debe aparecer dos o más veces. Por ejemplo, en Ada o en Modula-2, un bloque

o procedimiento tiene un identificador que aparece al comienzo y al final de la construcción. El compilador debe verificar que

el mismo identificador aparece en ambos lugares.

En este apartado se estudiará la comprobación de tipos en profundidad. La mayoría de los compiladores de Pascal

realizan en un solo paso:

- análisis sintáctico;

- comprobación de tipos;

- generación de código intermedio.

Sin embargo en lenguajes más complejos, como por ejemplo el Ada, es conveniente realizar la comprobación de tipos

en un paso separado, tal y como se muestra en la figura 2:

árbol árbol
GENERADOR
cadena sintáctico COMPROBACION sintáctico
ANALISIS DE código
de DE
SINTACTICO CODIGO intermedio
tokens TIPOS abstracto
INTERMEDIO

Figura 2

- 15 -
Análisis semántico en procesadores de lenguaje

La comprobación de tipos verifica que los tipos utilizados en un determinado contexto son los apropiados. Por ejemplo,

una construcción con el operador MOD del lenguaje Pascal exige que ambos operandos sean de tipo integer. De igual forma

que la comprobación de tipos debe verificar que se utilizan sólo índices con variables del tipo array, o que una función definida

por el ususario mantiene la correspondencia entre el número y tipo de parámetros declarados con los utilizados en la llamada.

En este apartado se mostrará la construcción de un pequeño modelo de comprobación de tipos. También se planteará

el problema de la equivalencia entre tipos y la conversión de tipos.

La información recogida por la comprobación de tipos, puede necesitarse para la generación de código. Por ejemplo,

el operador aritmético +, habitualmente, se utiliza para representar la adición entre enteros y reales, pero también se utiliza en

ocasiones sobre otros tipos de datos como string (concatenación), conjuntos (unión),... En este caso es necesario examinar el

contexto para determinar el significado del operador y consecuentemente generar el código correspondiente. Un símbolo que

puede representar diferentes operaciones en diferentes contextos se dice que está sobrecargado (overloaded).

La sobrecarga puede acompañarse de tipos inducidos, cuando un compilador obliga a un operando a convertirse al tipo

esperado por el contexto (si es posible).

Otro concepto deiferente a la sobrecarga de operadores es el concepto de polimorfismo. El cuerpo de una función

polimórfica puede ejecutarse con argumentos de varios tipos.

4.2 Sistemas de tipos


El diseño del módulo de comprobación de tipos para un determinado lenguaje de programación se basa en distintos

aspectos:

- las construcciones sintácticas del lenguaje;

- la noción de tipos;

- las reglas para asignar tipos a las construcciones del lenguaje.

Por ejemplo en Pascal, su manual de referencia indica:

"Si los dos operandos de los operadores aritméticos adición, sustracción y multiplicación son del tipo entero, entonces

el resultado es de tipo entero."

En cambio en los lenguajes C y FORTRAN, también debe añadirse el operador división.

Otro ejemplo en C, es el siguiente:

"El resultado del operador unario & es un puntero al objeto referenciado por el operando. Si el tipo del operando es

’...’, el tipo del resultado es ’puntero a ...’"

Como conclusión a los ejemplos anteriores es que cada expresión tiene un tipo asociado con ella. Además los tipos

pueden construirse, por ejemplo el tipo puntero a ... es un tipo construido a partir de ... y referido a él.

- 16 -
Análisis semántico en procesadores de lenguaje

En los lenguajes C y Pascal, los tipos pueden ser básicos o construidos. Los tipos básicos son aquellos cuya estructura

interna no puede ser modificada por el programador. En Pascal los tipos básicos son: boolean, carácter, entero y real. También

se tratan como tipos básicos los tipos subrango, y los tipos enumerados.

El lenguaje Pascal permite al programador construir tipos a partir de los tipos básicos o de otros tipos construidos con

arrays, registros y conjuntos. Además, los punteros y las funciones pueden tratarse también como tipos construidos.

Expresiones de tipo (type expresion)

El tipo de una construcción de un lenguaje puede denominarse type expresion (expresión de tipo). Intuitivamente, una

expresión de tipo es cualquier tipo básico o formado por aplicación de un operador, denominado constructor de tipos, a otra

expresión de tipo. El conjunto de tipos básicos y tipos construidos depende del lenguaje a comprobar semánticamente.

En este libro se utilizarán las siguientes definiciones de expresiones de tipo:

1. Un tipo básico es un tipo de expresión. Entre los tipos básicos se encuentran boolean, char, integer y real. Un tipo

básico especial, es el tipo_error, que indicará un error durante la comprobación de tipos. Finalmente el tipo básico void que

indica la ausencia de valor.

2. También se pueden nombrar las expresiones de tipo, un nombre de tipo es una expresión de tipo.

3. Un constructor de tipos aplicado a expresión de tipo, es una expresión de tipo. Los constructores de tipos son:

a) arrays: si T es una expresión de tipo, entonces array(I,T) es una expresión de tipo que indica el tipo de un array con

elementos de tipo T y subíndices I. I es a menudo un rango de enteros. Por ejemplo, en Pascal, la declaración:

VAR a: ARRAY [1..10] of integer

asocia a a la expresión de tipo array(1..10,integer) con a.

b)productos: Si T1 y T2 son expresiones de tipos, entonces su producto cartesiano T1xT2 es una expresión de tipos. Se
supone que x es asociativo por la izquierda.

c)registros: el tipo de un registro es, en sentido estricto, el producto de los tipos de sus campos. La diferencia entre un

registro y un producto es que los campos del registro tienen nombres. Por ejemplo, una tabla de símbolos puede construirse

según la estructura:

TYPE fila = RECORD


direccion: integer;
atributos: ARRAY [1..15] OF char
END;
VAR tabla_simbolos: ARRAY [1..101] OF fila;

La comprobación de tipos en el caso de registros puede realizarse usando una expresión de tipos formada al aplicar el

tipo constructor registro al duo formado por los nombres de los campos y sus nombres asociados.

RECORD ( (direccion x integer) x (atributos x array[1..15,char) )

Se pueden presentar algunos errores de programación:

- 17 -
Análisis semántico en procesadores de lenguaje

VAR x: RECORD
p_real: real;
p_imag: real
END;
y: RECORD
p_imag: real;
p_real: real
END;

La asignación de X a Y o de Y a X dará resultados semáticamente erróneos si la asignación se hace miembro a miembro,

aunque sea correcta según la estructura.

d) punteros: si T es una expresión de tipo, entonces puntero(T) es una expresión de tipo denominada puntero a un

objeto de tipo T. Por ejemplo: la declaración del lenguaje Pascal:

VAR p: ↑fila
declara una variable p de tipo puntero (fila).

e) funciones: matemáticamente una función es una aplicación entre dos conjuntos, el conjunto inicial o dominio, y el

final denominado rango.

f: D → R

Por ejemplo la función mod del lenguaje Pascal tiene como dominio integer x integer (un par de enteros) y como rango

integer. Así se tiene que:

mod: integer x integer → integer1

Otro ejemplo es la declaración de la siguiente función en Pascal

FUNCTION fabrica (a, b: char): ↑integer;


donde:

fabrica: char x char → puntero (integer)

A menudo, por razones de implementación, se limitan los tipos que puede devolver una función; por ejemplo, no se

suele permitir que devuelvan arrays o funciones. Sin embargo, existen lenguajes, como el LISP, que permiten a las funciones

devolver objetos arbitrarios, así se puede definir una función g de la forma:

g: (integer → integer) → (integer → integer)

es decir g toma como argumento una función que devuelve un entero, y produce como resultado otra función del mismo tipo.

4. Las expresiones de tipo pueden contener variables cuyos valores son también expresiones de tipo.

SISTEMAS DE TIPOS

Un sistema de tipos es un conjunto de reglas para asignar expresiones de tipo a varias partes del programa. La

comprobación de tipos implementa un sistema de tipos.

1 Se supone que x tiene más precedencia que → (es asociativa por la derecha)

- 18 -
Análisis semántico en procesadores de lenguaje

Pueden utilizarse diferentes sistemas de tipos en distintos compiladores de un mismo lenguaje de programación. Por

ejemplo, en Pascal, el tipo array incluye el conjunto de índices del array. Sin embargo, muchos compiladores obligan a especificar

el tamaño del array cuando se pasa como subíndice. Así, estos compiladores usan diferente sistema de tipos que el usado en la

definición del lenguaje. De forma similar, en el sistema operativo UNIX, el comando lint busca en los programas en C los

posibles gazapos con más detalle que el compilador de C.

COMPROBACION ESTATICA Y DINAMICA DE TIPOS

La comprobación de tipos en tiempo de compilación se denomina estática, mientras que la comprobación en tiempo

de ejecución se denomina dinámica.

En principio, cualquier comprobación puede realizarse dinámicamente, si el código objeto transporta el tipo de un

elemento con el valor del elemento.

Un sistema de tipos puro elimina la necesidad de la comprobación dinámica, dado que determina todos los errores en

tiempo de compilación. Es decir, un sistema de tipos puro con tipo "tipo_error" a una zona de un programa, y no permite que

se ejecute éste.

Un lenguaje se dice fuertemente tipeado si su compilador puede garantizar que los programas que acepta pueden

ejecutarse sin errores.

En la práctica, algunas comprobaciones deben realizarse siempre en tiempo de ejecución. Por ejemplo, si se declara:
VAR
vector: ARRAY [0..256] OF char;
i: integer;
y se calcula vector[i], un compilador no puede garantizar, en general, durante la ejecución del programa que el valor de i esté

comprendido entre 0 y 255.

TRATAMIENTO DE ERRORES

Una vez que se ha detectado un error en la comprobación de tipos, es necesario indicarlo al programador, señalando su

naturaleza y localización.

ESPECIFICACION DE UN COMPROBADOR DE TIPOS ELEMENTAL

Sea un lenguaje de programación en el cual es obligatorio declarar todos los identificadores antes de ser usados. Se

pretende construir un comprobador de tipos de este lenguaje, basado en sintetizar el tipo de una expresión a partir del tipo de

sus subexpresiones.

La gramática del lenguaje es la siguiente:

<P> símbolo inicial

Producciones

<P> ::= <D> ; <E>

- 19 -
Análisis semántico en procesadores de lenguaje

<D> ::= <D> ; <D> | id : <T>

<T> ::= char | integer | array [num] of <T> | ↑<T>

<E> ::= literal | num | id | <E> mod <E> | <E> [ <E> ] | <E>↑

donde

<D> representa las declaraciones

<T> representa a los tipos

<E> representa a las expresiones

Un ejemplo de programa en esta gramática es el siguiente:

clave: integer;
clave mod 1999

El lenguaje tiene dos tipos básicos: char e integer.

También existe un tercer tipo interno, el tipo_error, usado para indicar errores.

Para simplificar se supone que los arrays comienza en 1, así:

array [256] of char

es una expresión de tipo array (1..256,char), donde el constructor de tipos array se ha aplicado al subrango 1..256 y al tipo

char.

Al igual que el lenguaje Pascal, el operador prefijo ↑ en la zona de declaraciones construye un tipo puntero, por ejemplo:

↑integer

es una expresión del tipo puntero(entero), donde el constructor ↑ se aplicó al tipo entero.

Sea el siguiente esquema de traducción, que corresponde sólamente a la parte de declaraciones:

<P> → <D> ; <E>

<D> → <D> ; <D>

<D> → id : <T> { añade_tipo(id.entrada, T.tipo) }

<T> → char { T.tipo:= char }

<T> → integer { T.tipo:= integer }

<T> ↑<T1> { T.tipo:= puntero(T1.tipo) }

<T> → array [num] of <T1> { T.tipo:= array(1..num.val,T1.tipo) }

* La primera producción ( <P> → <D> ; <E> ) asegura que todos los identificadores sean declarados antes de que

se construyan expresiones.

* La segunda producción (<D> → <D> ; <D> ) permite realizar múltiples declaraciones.

- 20 -
Análisis semántico en procesadores de lenguaje

* La acción asociada con la producción ( <D> → id : <T> ) almacena el identificador "id" en la tabla de símbolos

asignándole el tipo <T>. Esta acción se puede indicar por:

añade_tipo(id.entrada, T.tipo)

donde tipo es un atributo del no terminal <T>.

* Si <T> deriva a char o integer, entonces T.tipo se define como char o integer respectivamente.

* De igual forma se trabaja con el resto de los tipos.

* El límite superior de los subíndices del array se obtiene con el atributo val de num.

COMPROBACION DE TIPOS EN EXPRESIONES

En las siguientes reglas se muestra la comprobación de tipos en expresiones:

<E> → literal { E.tipo:= char }

<E> → num { E.tipo:= integer }

<E> → id { E.tipo:= consulta(id.entrada) }

<E> → E1 mod E2 { E.tipo:= if E1.tipo= integer and

E2.tipo= integer

then integer

else tipo_error }

<E> → E1 [E2 ] { E.tipo:= if E2.tipo= integer and

E1.tipo= array(s,t) then t

else tipo_error }

<E> → E1↑ { T.tipo:= if E1.tipo= puntero(t) then t

else tipo_error }

* Se utiliza la función consulta(e) para que busque en la tabla de símbolos el tipo de la entrada e.

COMPROBACION DE TIPOS DE SENTENCIAS

A diferencia de las expresiones, las sentencias no son en general de ningún tipo. Se les puede asignar el tipo void (vacío)

si están correctas, o el tipo_error si incumplen la regla semántica.

En este ejemplo se considerarán las sentencias de asignación, las condicional y la sentencia repetitiva while. Las

secuencias de sentencias se separan por un punto y coma. Para tener en cuenta a las sentencias debe de añadirse a la gramática

de reglas:

<P> → <D> ; <S> ( sustituye a <P> → <D> ; <E> )

<S> → <S> ; <S> | <S>

Otras reglas, y sus comprobaciones semánticas son:

- 21 -
Análisis semántico en procesadores de lenguaje

<S> → id := <E> { S.tipo:= if id.tipo= E.tipo then void

else tipo_error }

<S> → if <E> then <S1> { S.tipo:= if E.tipo= boolean then S1.tipo


else tipo_error }

<S> → while <E> do <S1> { S.tipo:= if E.tipo=boolean then S1.tipo

else tipo_error }

<S> → <S1> ; <S2> { S.tipo:= if S1.tipo= void and

S2.tipo= void then void

else tipo_error }

COMPROBACION DE TIPOS DE FUNCIONES

La producción <E> → <E> ( <E> ) representa a una función de un solo argumento, en la cual una expresión es la

aplicación de una expresión en ora.

Las reglas para asociar expresiones de tipo con no terminales <T> pueden aumentarse con la siguiente producción:

<T> → <T1> ’→’ <T2>

{ T.tipo:= T1.tipo → T2.tipo }

’→’ las comillas son para diferenciar al constructor de funciones del metasímbolo que indica producción.

La regla para comprobar el tipo de una llamada a una función es:

<E> → <E1> ( <E2> )

{ E.tipo:= if E2.tipo= s and E1.tipo= s→t then t


else tipo_error }

Desde el punto de vista de la generalización a funciones de más de un argumento, puede indicarse que n argumentos

de tipos T1, T2, ..., Tn pueden verse como un solo argumento de tipo T1xT2xT3x...xTn.

4.3 Equivalencia de expresiones de tipo


Las reglas de comprobación de tipos anteriores son de la forma if dos expresiones de tipo son iguales then devuelve un

cierto tipo else devuelve tipo_error. Parece conveniente precisar la definición de cuando dos expresiones son equivalentes.

4.3.1 Equivalencia por estructura de expresiones de tipo


Se dice que dos expresiones de tipo tienen equivalencia por estructura si ambas se forman con el mismo constructor de

tipos.

Ejemplo:
TYPE MATRIZ= ARRAY [1..10] OF integer;
VAR
a: MATRIZ;
b: ARRAY [1..10] OF integer;
c: ARRAY [1..10] OF integer;

- 22 -
Análisis semántico en procesadores de lenguaje

a, b y c tienen equivalencia por estructura.

Algoritmo para comprobar la equivalencia por estructura

(1) function expresionesEquivalentes(s,t): boolean;

BEGIN

(2) if s y t de un mismo tipo básico then

(3) return true

(4) else if s= array(s1, s2) and t= array(t1, t2) then

(5) return expresionesEquivalentes(s1,t1) and

expresionesEquivalentes(s2,t2)

(6) else if s= s1 x s2 and t= t1 x t2 then

(7) return expresionesEquivalentes(s1,t1) and

expresionesEquivalentes(s2,t2)

(8) else if s= puntero(s1) and t= puntero(t1) then


(9) return expresionesEquivalentes(s1,t1)

(10) else if s= s1 → s2 and t= t1 → t2 then

(11) return expresionesEquivalentes(s1,t1) and

expresionesEquivalentes(s2,t2)

else

(12) return false

END

4.3.2 Equivalencia por identificador de expresiones de tipo


En este tipo de equivalencia, se considera que dos objetos son idénticos si son idénticos los identificadores de tipo. Este

tipo de equivalencia puede causar problemas en lenguajes que permiten asignar identificadores a los tipos. Por ejemplo, en el

fragmento de programa en Pascal, donde celda es un tipo definido por el usuario:

TYPE enlace= ↑celda;


VAR
siguiente: enlace;
anterior: enlace;
p: ↑celda;
q,r: ↑celda;
¿siguiente, anterior, p, q ,r son del mismo tipo?

Sorprendentemente la respuesta depende de la implementación, pues el lenguaje no define cuales son los tipos idénticos.

- 23 -
Análisis semántico en procesadores de lenguaje

4.3.3 Bucles en la representación de los tipos


Algunas estructuras de datos, como listas encadenadas y árboles, se definen frecuentemetne de forma recursiva; así una

lista encadenada puede ser vacía o es un nodo con un puntero que señala a la lista. Estas estructuras de datos se implementan

habitualmente utilizando registros que contienen punteros que señalan a registros similares, y los nombres de los tipos juegan

un papel fundamental en la definición de los tipos de tales registros.

Ejemplo:

Considérese una lista encadenada de celdas, cada una de ellas contiene una información que es un entero y un puntero

que señala a la siguiente celda en la lista. En Pascal se declararía de la siguiente forma:

TYPE enlace= ↑celda;


celda= RECORD
info: integer;
siguiente: enlace
END;
Nótese que el tipo enlace se define en función de celda, y celda se define en función de enlace, así, estas declaraciones

son recursivas.

celda= RECORD

x x

info integer siguiente enlace

Figura 3: Definición recursiva de CELDA

En lenguaje C:
struct celda {
int info;
struct celda *siguiente;
};

4.4 Conversión de tipos


Sea la expresión

x+i

donde:

x: real;

i: integer

En un principio el operador + realiza la adición si ambos operadores son del mismo tipo, en el caso de que no sean del

mismo tipo el lenguaje debe especificar que conversiones son necesarias.

- 24 -
Análisis semántico en procesadores de lenguaje

En general se convierten los enteros a reales y se realiza la operación real sobre ambos operadores reales.

La comprobación de tipos puede ser utilizada para insertar estas operaciones de conversión en la representación

intermedia del código fuente. Por ejemplo, usando una notación postfija, x+i se convierte en:

x i inttoreal real+

inttoreal : convierte i de entero a real

real+ : realiza la adición de dos operandos reales

COERCIONES (cast)

Una conversión de un tipo a otro se dice implícita si se realiza automáticamente por el compilador. Las conversiones

de tipo implícitas, tambien se denominan coerciones (coercions).

Una conversión se dice explícita si el programador debe escribir alguna sentencia, función, ... para realizar la conversión.

Ejemplos:

a) En Pascal, las funciones ord y chr convierten enteros a caracteres y viceversa. Conversión explícita.

b) En C, se convierte implícitamente los caracteres ASCII a enteros entre 0 y 127 en expresiones aritméticas. Coerción.

c) Sean las expresiones formadas al aplicar el operador aritmético op a constantes y operandos según la gramática:

PRODUCCION REGLA SEMANTICA

<E> → num E.tipo:= integer

<E> → num.num E.tipo:= real

<E> → id E.tipo:= consulta(id.entrada)

<E> → <E1> op <E2> E.tipo:= if E1.tipo= integer and

E2.tipo= integer then integer

else if E1.tipo= integer and

E2.tipo= real then real

else if E1.tipo= real and

E2.tipo= integer then real

else if E1.tipo= real and

E2.tipo= real then real

else tipo_error

d) La conversión implícita de constantes se realiza habitualmente en tiempo de compilación, pero también en algunos

casos se realiza en tiempo de ejecución. Por ejemplo, puede teclearse el siguiente código en Pascal:
FOR i:= 1 TO n DO x[i]:= 1;
comprobar su tiempo de compilación y de ejecución.

Modificar el fragmento anterior por:


FOR i:= 1 TO n DO x[i]:= 1.0

- 25 -
Análisis semántico en procesadores de lenguaje

y observar los nuevos tiempos de compilación, y de ejecución.

Si aumenta el tiempo de compilación, está claro que la conversión automática se realiza en tiempo de compilación. Si

aumenta el tiempo de ejecución se realiza en tiempo de ejecución.

4.5 Sobrecarga de funciones y operadores


Un símbolo está sobrecargado (overloaded) si tiene diferentes significados según su contexto. Por ejemplo en

Matemáticas el operador + está sobrecargado, dado que + en A+B tiene diferentes significados en función de que A y B sean

enteros, reales, números complejos o matrices. Otro ejemplo es el paréntesis () en Ada; la expresión A(I) puede ser el I-ésimo

elemento del array A, una llamada a la función A con el argumento I, o una conversión explícita de la expresión I a tipo A.

La sobrecarga se resuelve cuando se determina el significado correcto del símbolo sobrecargado. Por ejemplo en la

expresión:

x + (i + j)

el operador + puede indicar diferentes formas de adición dependiento de los tipos de x, i y j. La resolución de la sobrecarga en

este cso se realiza identificando los operandos.

Los operadores aritméticos están sobrecargados en la mayoría de los lenguajes. Sin embargo su resolución se realiza

observando los tipos de los operandos. Las reglas semánticas a utilizar son las mismas que en el caso de <E1> op <E2> (apartado

4.4).

4.5.1 Conjunto de posibles tipos para una subexpresión


No siempre es posible resolver la sobrecarga observando sólo los tipos de los operandos, o los tipos de los argumentos

de una función.

Ejemplo:

En Ada, una de las interpretaciones predefinidas del operador "*" es una función de una pareja de enteros a un entero.

El operador puede ser sobrecargado añadiendo declaraciones como las siguientes:

function "*"(i,j: integer) return complex;


function "*"(x,y: complex) return complex;

Después de estas declaraciones, los posibles tipos para * son:

integer x integer → integer

integer x integer → complex

complex x complex → complex

Así:

3 * 5 puede ser integer o complex, dependiendo del contexto.

2 * (3 * 5) entonces (3 * 5) → integer

(3 * 5) * z entonces (3 * 5) → complex, si z → complex

- 26 -
Análisis semántico en procesadores de lenguaje

Hasta ahora se suponía que cada expresión tenía un único tipo, y que la regla para la comprobación de tipos era de la

siguiente forma:

PRODUCCION REGLA SEMANTICA

<E> → <E,> (<E2>) { E.tipo:= if E2.tipo= 1 and E1.tipo= 1→ t then t

else tipo_error }

Esta regla semántica debe generalizarse al conjunto de posibles tipos de una expresión:

PRODUCCION REGLA SEMANTICA

<E’> → <E> { E’.tipo:= E.tipo }

<E> → id { E.tipo:= consulta(id.entrada) ↓

<E> → <E1> (<E2>) { E.tipo:= { t/ existe un s ∈ { E2.tipo } tal que s → t, t ∈{ E1.tipo } } }

En este caso se supone que la tabla de símbolos puede contener el conjunto de posibles tipos. Puede reservarse el

conjunto vacío, como un almacenamiento temporal del tipo_error.

4.5.2 Reducción del conjunto de posibles tipos


El lenguaje Ada obliga a que una expresión completa tenga un tipo único. Dado que se conoce el tipo de la expresión

completa, se pueden reducir los tipos posibles en cada una de las subexpresiones. Si en este proceso no resultase un tipo único

para cada subexpresión se devolvería tipo_error.

Antes de realizar la descomposición descendente de una expresión en subexpresiones, se pueden observar los conjuntos

de tipos posibles contruidos con las reglas semánticas del apartado anterior. Se muestra que cualquier tipo t ∈ { E.tipos } es un

tipo factible; por ejemplo es posible escoger entre los tipos sobrecargados de los identificadores presentes en la expresión E

de tal forma que E tome el tipo t. Las propiedades asignadas a los identificadores en su declaración hacen que cada elemento

de id.tipos sea factible.

Existen distintos caminos para llegar a que un tipo sea factible. Por ejemplo, considérese la expresión f(x) donde f puede

tener los tipos a→c y b→c, y x puede tener los tipos a y b. Entonces, f(x) tiene tipo c pero x puede ser de tipo a y b.

Parece conveniente por tanto modificar las reglas semánticas de la sección anterior, introduciendo dos nuevos tipos de
atributos único y código.

- 27 -
Análisis semántico en procesadores de lenguaje

PRODUCCION REGLAS SEMANTICAS

<E’>→<E> E’.tipos:= E.tipos

E.único:= if E’.tipos then t

else tipo_error

E’.código:= E.código

<E>→id E.tipos:= consulta(id.entrada)

E.código:= genera(id.lexema ’:’ E.único)

<E>→<E1>(<E2>) E.tipos:= {s’/∃ s ∈ {E2.tipos} tal que s→s’ ∈ {E1.tipos} }

t:= E.único

S:= {s/s ∈ {E2.tipos} and s→t ∈ {E1.tipos} }


E2.único:= if S={s} then s else tipo_error
E1.único:= if S={s} then s→t else tipo error

E.código:= E1.código ||

E2.código ||

genera(’apply’ ’:’ E.único)

El atributo único indica que sólo se admite este tipo, y se puede propagar de forma descendente de las expresiones a

las subexpresiones.
El atributo código se utiliza para ayudar a la generación de código intermedio.

4.6 Funciones polimórficas


Normalmente, los procedimientos y las funciones ejecutan las sentencias que forman su cuerpo con unos argumentos

de tipo fijo. Los procedimientos y funciones polimórficas se pueden ejecutar cada vez con argumentos de diferente tipo. También

se puede hallar de operadores polimórficos.


Ejemplo:

El operador & en C se describe como: "si el tipo del operando es ’...’, el tipo de resultado es ’puntero a ...’". Dado que

cualquier tipo se puede sustituir por ’...’, el operador & es polimórfico.

Este apartado está dirigido a plantear los problemas de la comprobación de tipos para un lenguaje con funciones poli-

mórficas.

¿Para qué sirven las funciones polimórficas?

- 28 -
Análisis semántico en procesadores de lenguaje

Las funciones polimórficas tienen su principal utilidad en el manejo de estructuras de datos. Por ejemplo, puede ser

conveniente tener una función que determine la longitud de una lista, sin tener que conocer como son los elementos de la lista,

a continuación se presenta cómo se podría hacer en Pascal.

TYPE enlace= ↑celda;


celda= RECORD
info: integer;
siguiente: enlace
END;

FUNCTION longitud(lptr: enlace): integer;


VAR
lon: integer;
BEGIN
lon:= 0;
WHILE lptr<>NIL DO
BEGIN
lon:= lon+1;
lptr:= lptr↑.next
END;
longitud:= lon
END;

Sin embargo no se ha cumplido nuestro propósito dado que los lenguajes como el Pascal obligan a especificar com-

pletamente el tipo de los parámetros de la función. Así esta función deberá de modificarse para calcular la longitud de una lista

encadenada de reales.

fun longitud(lptr)=
if null(lptr) then 0
else longitud(t1(lptr))+1

Una función polimórfica en ML

fun → indica que es una función recursiva.

null → comprueba si la lista está vacía.

t1 → devuelve el siguiente elemento de la lista.

Con el programa anterior en ML se puede calcular:

longitud(["lunes","martes","miercoles"]);
longitud([10,9,8,7])

El primer caso es una lista de cadenas, el segundo una lista de enteros.

BIBLIOGRAFIA
AHO86 Aho A.V. , R. Sethi and J.D. Ullman. Compilers: Principles, techniques, and tools. Addison-Wesley, 1986.

Versión castellana: Compiladores: Principios, técnicas y herramientas. Addison-Wesley Iberoamericana,

1990.

CAST93 Castro J., Cucker F., Messeguer X., Rubio A., Solano L., Vallés B. Curso de programación. Ed. McGraw-Hill,

1993.

- 29 -
Análisis semántico en procesadores de lenguaje

CUEV91 Cueva Lovelle, J. M. Lenguajes, Gramáticas y Autómatas. Cuaderno Didáctico nº36, Dto. de Matemáticas,

Universidad de Oviedo, 1991.

CUEV93a Cueva Lovelle, J. M. Análisis léxico en procesadores de lenguaje. Cuaderno Didáctico nº48, Dto. de

Matemáticas, Universidad de Oviedo, 2ª edición, 1993.

CUEV94 Cueva Lovelle J. M., Mª P. A. García Fuente, B. López Pérez, Mª C. Luengo Díez, y M. Alonso Requejo.

Introducción a la programación estructurada y orientada a objetos con Pascal. Cuaderno Didáctico nº 69,

Dto. de Matemáticas, Universidad de Oviedo, 1994.

CUEV94b Cueva Lovelle, J.M. Conceptos básicos de Traductores, Compiladores e Intérpretes. Cuaderno Didáctico nº9,

Dto. de Matemáticas, Universidad de Oviedo, 4ª Edición, 1994.

CUEV95 Cueva Lovelle, J.M. Análisis sintactico en procesadores de lenguaje. Cuaderno Didáctico nº 61, Dto. de

Matemáticas, Universidad de Oviedo, 2ª Edición, 1995.

DAWE91 Dawes J. The VDM-SL reference guide. UCL Press, 1991.

FRAS94 M.D. Fraser, K. Kumar, V.K. Vaishnavi. "Strategies for incorporating formal specifications in software

development". Communications of the ACM, Vol. 37, No. 10, pp. 74-85, October 1994.

HEKM88 Hekmatpour S., Ince D. Software prototyping, formal methods and VDM. Addison-Wesley, 1988.

HOLU90 Holub A.I. Compiler design in C. Prentice-Hall, 1990.

LEIV93 Leiva Vázquez J.A. y Cueva Lovelle J.M. Construcción de un traductor de lenguaje Eiffel a C++. Cuaderno

didáctico nº 76. Departamento de Matemáticas. Universidad de Oviedo, 1993.

LEVI79 Levitt K. et al. The HDM handbook. SRI International, 1979.

TREM85 Tremblay J. P. and P.G. Sorenson. The theory and practice of compiler writing. Ed. McGraw-Hill, 1985.

WATT91 Watt D.A. Programming Language Syntax and Semantics. Prentice-Hall, 1991.

WATT93 Watt D.A. Programming Language Processors. Prentice-Hall, 1993.

- 30 -
Análisis semántico en procesadores de lenguaje

Tabla de Contenidos
1 INTRODUCCION .................................................................................................................................................. 1

2 ESPECIFICACIONES SEMANTICAS DE LOS LENGUAJES DE PROGRAMACION .................................. 1


2.1 Especificación natural o informal ................................................................................................................ 1
2.1.1 Compatibilidad de tipos .................................................................................................................... 1
2.1.2 Rutinas semánticas ............................................................................................................................ 1
2.2 Especificación formal .................................................................................................................................. 1
2.2.1 Gramáticas atribuidas o gramáticas con atributos ............................................................................. 2
2.2.2 Lenguajes de especificación semántica ............................................................................................. 2
2.2.2.1 Lenguajes orientados a definir un modelo .............................................................................. 2
2.2.2.2 Lenguajes orientados a definir propiedades ........................................................................... 3
2.2.2.3 Metodologías de desarrollo de software ................................................................................. 3
2.2.2.3.1 VDL (Vienna Definition Language) ............................................................................ 3
2.2.2.3.2 VDM (Vienna Development Method) .......................................................................... 3
2.2.2.3.3 HDM (Hierarchical Development Method) ................................................................. 4
2.2.2.3.4 ANNA (ANNotated Ada) ............................................................................................. 4
2.2.2.3.5 GYPSY ......................................................................................................................... 5

3 GRAMATICAS CON ATRIBUTOS ..................................................................................................................... 6


3.1 Atributos ...................................................................................................................................................... 6
3.1.1 Ejemplo de atributos .......................................................................................................................... 6
3.2 Reglas semánticas ........................................................................................................................................ 7
3.2.1 Ejemplo de reglas y condiciones semánticas .................................................................................... 7
3.2.2 Ejemplo de evaluación de atributos mediante reglas semánticas ...................................................... 9
3.3 Atributos heredados ..................................................................................................................................... 10
3.3.1 Ejemplos de atributos heredados ....................................................................................................... 10
3.4 Atributos sintetizados .................................................................................................................................. 10
3.4.1 Ejemplos de atributos sintetizados .................................................................................................... 10
3.5 Definición formal de gramáticas con atributos ............................................................................................ 10
3.5.1 Definición formal de atributos calculados ......................................................................................... 11
3.5.2 Definición formal de atributos sintetizados ....................................................................................... 11
3.5.3 Definición formal de atributos heredados ......................................................................................... 11
3.5.4 Gramática con atributos completa ..................................................................................................... 12
3.5.5 Gramáticas con atributos bien definidas ........................................................................................... 12
3.5.6 Teorema de las gramáticas bien definidas ......................................................................................... 12
3.6 Gramáticas S-atribuidas ............................................................................................................................... 12
3.6.1 Evaluación ascendente de gramáticas S-atribuidas ........................................................................... 12
3.7 Gramáticas L-atribuidas ............................................................................................................................... 12
3.7.1 Evaluación descendente de gramáticas L-atribuidas ......................................................................... 12
3.8 Esquemas de traducción atribuidos .............................................................................................................. 13
3.8.1 Esquemas de traducción atribuidos bien definidos ........................................................................... 13
3.9 Generador de compiladores basado en gramáticas atribuidas ..................................................................... 13

4 COMPATIBILIDAD DE TIPOS ........................................................................................................................... 15


4.1 Introducción ................................................................................................................................................. 15
4.2 Sistemas de tipos .......................................................................................................................................... 16
4.3 Equivalencia de expresiones de tipo ............................................................................................................ 22
4.3.1 Equivalencia por estructura de expresiones de tipo .......................................................................... 22
4.3.2 Equivalencia por identificador de expresiones de tipo ...................................................................... 23
4.3.3 Bucles en la representación de los tipos ............................................................................................ 24
4.4 Conversión de tipos ..................................................................................................................................... 24
4.5 Sobrecarga de funciones y operadores ......................................................................................................... 26
4.5.1 Conjunto de posibles tipos para una subexpresión ............................................................................ 26
4.5.2 Reducción del conjunto de posibles tipos ......................................................................................... 27
4.6 Funciones polimórficas ................................................................................................................................ 28

BIBLIOGRAFIA ....................................................................................................................................................... 29

- ii -
Análisis semántico en procesadores de lenguaje

Tabla de figuras
Fig. 1: Arbol sintáctico con atributos ........................................................................................................................ 9
Fig. 1 .......................................................................................................................................................................... 13
Fig. 2 .......................................................................................................................................................................... 15
Fig. 3: Definición recursiva de CELDA .................................................................................................................... 24

- iii -

Você também pode gostar