Está en la página 1de 41

Compiladores e Interpretes

La Tabla de Símbolos
Luis Ochoa
ziul1979@gmail.com
Introducción
• Tabla de Símbolos:
▫ La tabla de símbolos es la estructura de datos
utilizada por el compilador para gestionar los
nombres (identificadores) que aparecen en el
programa fuente: las constantes, las variables, los
tipos, las acciones, etc.
▫ Cuando el compilador encuentra un identificador,
guarda en esta tabla la información que lo
caracteriza: su nombre, su categoría (acción,
variable, constante, clase, tipo, etc.), la dirección de
memoria que se le asigna, su tamaño y el resto de
información, que depende de la categoría concreta.
Introducción
▫ Cuando el identificador es referenciado en el programa, el
compilador consulta la tabla de símbolos y obtiene la
información que necesita.
▫ Una vez se acaba el ámbito de definición del identificador,
se elimina y desaparece de la tabla de símbolos.
▫ RESUMIENDO:
 La tabla de símbolos es una estructura global utilizada por
distintos módulos del compilador
 Contiene una entrada para cada uno de los símbolos definidos
en el programa fuente.
 Sobre los identificadores, y opcionalmente sobre las palabras
reservadas y las constantes.
 Información sobre el lexema, tipo de datos, ámbito y dirección
en memoria.
Uso de la tabla de símbolos durante
las fases del proceso de compilación
• El analizador léxico lee los caracteres del
código fuente y detecta los lexemas que
coinciden con los patrones de los testigos.
Cuando encuentra un identificador comprueba
si está introducido en la tabla de símbolos y, si
no lo está, crea una nueva entrada para el
mismo.
Uso de la tabla de símbolos durante
las fases del proceso de compilación
• El analizador sintáctico generalmente sólo
trabaja con los símbolos formados en la fase
anterior y añade información a los campos de
atributos. Sin embargo, en algunos casos puede
crear nuevas entradas en la tabla, como cuando
se define un nuevo tipo de datos que se debería
tratar como una palabra reservada y no como un
identificador.
Uso de la tabla de símbolos durante
las fases del proceso de compilación
• El conjunto de rutinas del análisis semántico,
que verifican el significado del programa fuente,
debe acceder a la tabla para consultar el tipo de
datos de los símbolos.
• El generador de código puede:
▫ Extraer información de la tabla. Por ejemplo, leer el
tipo de datos de una variable para poder saber cuánto
espacio le tiene que reservar el programa objeto.
▫ Guardar información en la tabla. Por ejemplo, debe
guardar la dirección de memoria en la que se situará
una variable. Cada vez que la variable aparezca en el
programa objeto, debe sustituirse por esta dirección.
Uso de la tabla de símbolos durante
las fases del proceso de compilación
• Nota Importante: Hay que recordar que el
compilador es el que utiliza la tabla de símbolos, y
no el programa compilado. La tabla de símbolos no
está presente en la ejecución del programa, excepto
cuando se incluye un depurador simbólico en la fase
de ejecución o en los casos de los intérpretes en los
que la compilación y la ejecución se alternan.
• También es importante conocer, que la tabla de
símbolos se puede inicializar con las palabras
reservadas del lenguaje para que el analizador
léxico deba verificar menos patrones, tenga menos
estados y se ejecute con mayor rapidez.
Estructura de los registros de la tabla
de símbolos
• Los registros que componen la tabla de símbolos
suelen tener una longitud fija y contienen el
nombre del símbolo y una serie de atributos
como la dirección de memoria, el número de
línea en el que se ha declarado, el tipo de dato,
etc.
Estructura de los registros de la tabla
de símbolos
• Los campos de estos registros pueden guardar
directamente la información para la que están
destinados, o bien un apuntador a la posición de
memoria en la que se guarda esta información, y de esta
manera reducir, en muchos casos, el espacio total
ocupado.
Estructura de los registros de la tabla
de símbolos
• Según el lenguaje que se compila y la estructura
del compilador, los campos del registro pueden
variar. Veremos a continuación los campos
principales más detalladamente:
▫ Nombre del símbolo. En este campo se
guardan todos los lexemas de los símbolos del
programa fuente (identificadores, constantes
numéricas y cualquier otro elemento que el
lenguaje sea capaz de gestionar). Para guardar los
lexemas se pueden utilizar dos técnicas básicas:
Estructura de los registros de la tabla
de símbolos
 Interna: El campo de la tabla de símbolos se define
como de tipo cadena de caracteres de dimensión fija (por
ejemplo 127 o 255). Los inconvenientes de esta técnica
son los siguientes:
 La longitud máxima de los identificadores está restringida al
valor fijado.
 Se desaprovecha mucho espacio, ya que los identificadores
no suelen ocupar todos los caracteres asignados.

 Externa: El campo de la tabla de símbolos contiene dos


nuevos campos:
 Un apuntador a la zona de memoria en la que se almacena el
lexema (Inicio).
 Un número con la longitud del lexema (Longitud).
Estructura de los registros de la tabla
de símbolos
▫ Número de línea de la declaración del símbolo o números de
línea en los que aparece el símbolo. La información que se guarda
en este campo es una información auxiliar que será de utilidad
para confeccionar un listado de referencias cruzadas que ayuden
a depurar los programas.
Se puede utilizar una lista encadenada de números de líneas; la
primera posición indica dónde aparece la declaración del
símbolo. Es un campo que llena el analizador léxico.

▫ Tipo: En este campo se guarda el código correspondiente al tipo


de dato representado por el símbolo, o un apuntador a la
estructura de datos correspondiente al tipo. El analizador
sintáctico es el encargado de llenar este campo.
Estructura de los registros de la tabla
de símbolos
▫ Dirección de memoria: En este campo se almacena
la dirección de memoria en la que se guardará el valor
correspondiente al símbolo durante la ejecución del
programa.
Normalmente, el dato que interesa guardar no es la
dirección absoluta, sino la referente al comienzo de la
memoria estática, el comienzo del registro de
activación correspondiente a la subrutina, etc.

 Es un dato que llena el generador de código durante su


proceso de inicialización y que utiliza después para
sustituir en el código objeto generado el nombre del
símbolo por su dirección de memoria.
Estructura de los registros de la tabla
de símbolos
• En caso de tipos de datos “complejos” los datos que
se almacenan pueden variar como:
▫ Para un array:
 Tipo de los elementos.
 Número de elementos.
 Límites inferior y superior.

▫ Para una función:


 Número de parámetros.
 Tipo de los parámetros.
 Forma de paso de parámetros.
 Tipo de retorno.
Operaciones de la tabla de símbolos
• La tabla de símbolos funciona como una estructura
de base de datos en la que el campo clave es el
lexema del símbolo. Las operaciones que se ejecutan
sobre esta estructura son las siguientes:
▫ Buscar un lexema y localizar el contenido de sus
atributos.
▫ Insertar un nuevo registro y comprobar
previamente que no haya otro con el mismo nombre.
▫ Modificar la información contenida en un registro.
En general, sólo se añade información a los campos
vacíos de los registros. La modificación siempre
requiere una búsqueda previa.
Operaciones de la tabla de símbolos
• En lenguajes con estructura de bloque se
incluyen dos operaciones más:
▫ Nuevo bloque: comienzo de un nuevo bloque.
▫ Fin de bloque: final del ámbito de un bloque.
En general, las operaciones más frecuentes son las de consulta. Se
realizan para ver si el símbolo ya estaba introducido antes de cada
inserción y de cada modificación. Durante el funcionamiento del
compilador no se suelen borrar registros de las tablas de símbolos,
excepto en los lenguajes con estructura de bloques. En este tipo de
lenguaje, al acabar el análisis de un bloque se deben eliminar de la tabla
todos los símbolos locales del bloque.

La tabla de signos acceden todas las fases del compilador. Si el


programa es largo, puede tener centenares o miles de entradas.
Por lo tanto, para un buen rendimiento del compilador es
esencial asegurar la eficiencia de las rutinas de gestión de la tabla.
Organización de la tabla de símbolos
• Una tabla de símbolos se puede organizar
básicamente de dos maneras:
▫ Tablas no ordenadas. Son generadas con
vectores o listas. En general, son métodos muy
poco eficientes pero fáciles de programar.

▫ Tablas ordenadas. Programadas utilizando


estructuras de tipo vector ordenado, listas
ordenadas, árboles binarios, árboles equilibrados
(AVL), tablas de dispersión (hash), etc.
Organización de la tabla de símbolos
• Debido a las características específicas de
funcionamiento de las tablas de símbolos, los
árboles equilibrados y las tablas de dispersión
resultan los métodos más eficientes. Este tipo de
organizaciones son un problema
convencional de programación y no es
materia de compiladores e interpretes el
volverlas a estudiar.

• Sin embargo, cómo solucionar la problemática


añadida al tratar con lenguajes con estructura por
bloques si es de interés en este campo.
Organización de la tabla de símbolos
en programas por bloques
• Los lenguajes con estructura por bloques están compuestos
por módulos o bloques de código de ejecución secuencial que
pueden contener submódulos y llamadas a otros módulos.

• Generalmente, un símbolo es accesible dentro del módulo en


el que está definido y en todos los módulos o submódulos que
contiene (de nivel inferior).

• Si en un submódulo se define un símbolo con un lexema igual


a uno que ya se ha declarado en un módulo superior, la
definición del nivel inferior predomina sobre la que ya había:
el lexema se asocia siempre a la definición más reciente.
Organización de la tabla de símbolos
en programas por bloques
• Las reglas de ámbito, propias de cada lenguaje, son el
conjunto de reglas semánticas que determinan qué
definición corresponde a cada lexema en cada momento
de la compilación.
▫ A continuación utilizaremos el ejemplo de un programa
escrito en un lenguaje con estructura por bloques para
estudiar el contenido de la tabla de símbolos en los puntos
(1) y (2) para cada una de las cuatro organizaciones en las
que profundizaremos en los puntos siguientes.

La columna de nivel indica la profundidad a la que se


encuentra la línea de programa y la columna de bloque
indica el número de bloque de la línea del programa:
Organización de la tabla de símbolos en
programas por bloques
Tablas de símbolos no ordenadas
• En el caso más sencillo, las tablas de símbolos se
construyen con un vector o con una lista. Se suele utilizar
una pila auxiliar de apuntadores de índice de bloque
para marcar el comienzo (o el final) de los símbolos
correspondientes a un bloque.

Cada vez que empieza un nuevo bloque se añade un


puntero a la pila de índice de bloque, que señala el
primer símbolo del bloque.

Al terminar de compilar el bloque, se eliminan todos


los símbolos que hay desde el siguiente al marcado por el
apuntador del bloque, hasta el final de la tabla.
Tablas de símbolos no ordenadas
▫ En el ejemplo anterior hay seis bloques. Cuando la
compilación llega a los puntos (1) y (2), el
contenido de las pilas es el que se muestra en los
esquemas de la figura siguiente:
Tablas de símbolos no ordenadas

• En esta organización se pueden hacer las


operaciones siguientes:
▫ Inserción. Cuando se encuentra la definición de un
símbolo, se debe verificar que no esté definido en el
último bloque, es decir, en el conjunto de registros que
va desde el que ha apuntado el último índice del
bloque hasta el final de la lista. Si no estaba
introducido, se inserta en la última posición de la pila.

En cambio, si ya estaba en la tabla, debe dar un error


de tipo ‘el símbolo ya estaba definido en este ámbito’.
Tablas de símbolos no ordenadas
▫ Búsqueda. La búsqueda de un registro en la
tabla se hace en sentido inverso a una inserción
(del último introducido hasta el primero de la
tabla). Así pues, cuando se encuentra el lexema
buscado, el símbolo corresponderá a la definición
hecha en el bloque más próximo.

▫ Nuevo bloque. Cuando empieza un nuevo


bloque, se hace una nueva entrada en la pila de
índice de bloque que apunte a la cima actual de la
tabla de símbolos.
Tablas de símbolos no ordenadas
▫ Final de bloque. Cuando termina un bloque, se
deben eliminar todos los registros que se ha
definido, es decir, todos los que van del siguiente
al apuntado por la última entrada de la pila de
índices de bloque hasta el final de la tabla de
símbolos. Después, también se elimina la última
entrada de la pila de índices de bloque.
Tablas de símbolos con estructura de
árbol único
• Para mejorar la eficiencia, se puede estructurar la
tabla de símbolos como un árbol binario (normal o
equilibrado) y añadir un nuevo campo a los registros
de la tabla que indique el número del bloque al cual
pertenece el símbolo.

Para solucionarlas posibles duplicidades de


lexemas iguales en distintos bloques, se utiliza una
lista encadenada en los nodos del árbol. Siguiendo
con el ejemplo obtendríamos los esquemas de la
figura siguiente:
Tablas de símbolos con estructura de
árbol único
• Nota: Se ha utilizado una estructura de árbol binario en la que el hijo
derecho es mayor que el padre, y el hijo izquierdo, menor.
Tablas de símbolos con estructura de
árbol único
• Sobre esta organización de tabla de símbolos se
pueden hacer las operaciones siguientes:
▫ Inserción. Para insertar un nuevo símbolo, primero
se busca la posición que le corresponde. Si ya hay un
registro con el mismo lexema, se comprueba que el
campo de bloque no se corresponda con el bloque
activo.
Si fuera así, se debería dar un error de tipo ‘el
símbolo ya estaba definido en este ámbito’. Si, por el
contrario, todo es correcto, en este nodo del árbol se
crea una lista en la que la cabeza será el nuevo registro.
Si no hay ningún registro con el mismo lexema, se
inserta directamente en el árbol.
Tablas de símbolos con estructura de
árbol único
▫ Búsqueda. La búsqueda de un registro en la
tabla se hace como en cualquier árbol binario, y no
es necesario recorrer las listas que puedan
aparecer en un nodo, ya que la estrategia de
inserción asegura que el nodo del árbol binario
contiene la definición más reciente del símbolo.

▫ Nuevo bloque. Sólo es necesario incrementar en


1 la variable con el número de bloque activo
(nivel). Cuando se inserta un nuevo símbolo, ya lo
hará en el ámbito correcto.
Tablas de símbolos con estructura de
árbol único
▫ Final de bloque. Al acabar la compilación de un bloque es
necesario eliminar los símbolos locales del bloque, y para
esto se debe:
 Localizar los registros del árbol correspondientes al bloque
activo.
 Borrarlos del árbol.
 Decrementar en 1 la variable con el número de bloque activo.

▫ Inconvenientes: Estas operaciones requieren bastantes


cálculos, lo que constituye el principal inconveniente del
método. Esto es especialmente cierto en el caso de árboles
equilibrados, en que la eliminación de nodos desequilibra el
árbol y es necesario reesctructurarlo.
Tablas de símbolos con estructura de
bosque de árboles
• Esta técnica utiliza un árbol para cada bloque del
programa y, como en el primer caso, una pila de índices
de bloque que apuntan a la raíz del árbol del bloque. Los
contenidos en los puntos (1) y (2) serían entonces los que
se muestran en la figura siguiente:
Tablas de símbolos con estructura de
bosque de árboles
• Las operaciones que se pueden hacer sobre esta
estructura de tabla de símbolos son las siguientes:
▫ Inserción. Consiste en hacer un alta normal en el
árbol del bloque activo.

▫ Búsqueda. La búsqueda de un registro es más


compleja que en los casos anteriores. Primero se busca
el lexema en el árbol del bloque activo. Si no se
encuentra, se busca en el árbol del bloque anterior, y
así sucesivamente hasta localizarlo o hasta que se
hayan rastreado todos los árboles.
Tablas de símbolos con estructura de
bosque de árboles
▫ Nuevo bloque. Cuando empieza la compilación
de un nuevo bloque, se crea un nuevo elemento en
la pila de índices de bloques y una nueva
estructura de tipo árbol que, de entrada, está
vacía.

▫ Final de bloque. Cuando se acaba la


compilación de un bloque, se destruye toda la
estructura de árbol asociada y se elimina el último
puntero de la pila de índices de bloques.
Tablas de símbolos de dispersión
(hash)
• Recordemos que una tabla de dispersión aplica
una función matemática al lexema para
determinar la posición de la tabla que le
corresponde y, de esta manera, poder
distribuirlos uniformemente.
El inconveniente de las tablas de dispersión
son las colisiones que se producen cuando dos
lexemas quieren ocupar la misma posición.
Tablas de símbolos de dispersión
(hash)
• Este problema se puede resolver básicamente de dos
Maneras:
▫ Tablas de dispersión cerradas. Se resuelven las
colisiones dentro de la misma tabla. Cuando se produce una
colisión, se recorre la tabla secuencialmente hasta
encontrar la posición vacía siguiente, en la que se inserta el
nuevo símbolo.

▫ Tablas de dispersión abiertas. Se utiliza una lista


encadenada de desbordamiento para resolver las colisiones.
Cada registro de la tabla tiene un nuevo campo de tipo
apuntador al registro. Cuando se produce una colisión, se
crea una lista encadenada con la cabeza en el registro de la
tabla.
Tablas de símbolos de dispersión
(hash)
• Si utilizamos este segundo tipo de tabla de dispersión, de la misma manera que con la
estructura de tipo árbol, se añade a los registros de la tabla un nuevo campo que
indica el número del bloque (nivel) al que pertenece el símbolo y un apuntador al
registro para formar las listas en caso de colisión. Los contenidos de la estructura en
los puntos (1) y (2) serían los que se muestran en la figura siguiente:
Tablas de símbolos de dispersión
(hash)
• Las operaciones sobre esta tabla son las siguientes:
▫ Inserción. La inserción se hace como en cualquier
tabla de dispersión. El tratamiento de las colisiones
más indicado es el uso de listas de desbordamiento,
aunque puede utilizar cualquier otro sistema con la
condición de que incluya los símbolos más recientes en
las primeras posiciones.

▫ Búsqueda. Como en cualquier tabla de dispersión, en


caso de colisión de la clave que retorna la función de
dispersión, se debe buscar en la zona de
desbordamiento hasta encontrar el símbolo o una
posición vacía
Tablas de símbolos de dispersión
(hash)
• Nuevo bloque. Sólo hay que registrar el cambio
del bloque activo.
• Final de bloque. Al acabar la compilación de un
bloque, es necesario localizar todos los registros
correspondientes al bloque activo y borrarlos de la
tabla.
Para hacerlo, hay que recorrer prácticamente
toda la tabla y, en el caso de una tabla de dispersión
cerrada, la eliminación de registros cuando hay
colisiones no es precisamente una tarea sencilla. Al
final, también hay que registrar el cambio del bloque
activo.
Definición de un nodo
• Según el tipo de identificador (constante, variable,
tipo, acción, función, etc.), la información que se
guarda en el registro varía. Por este motivo, en lugar
de tener una sola tabla de símbolos con un único
tipo de registro, se suele tener una tabla de símbolos
para cada tipo de identificador.

• La tabla siguiente muestra algunos de los campos


que utiliza Pascal en las diferentes tablas de
símbolos que se pueden emplear para los distintos
tipos de identificadores:
Definición de un nodo

También podría gustarte