Escuela Técnica Superior de Ingeniería

 

Grado en Ingeniería Informática

Procesadores de Lenguajes

Curso 2022/2023

 

Práctica 1
Características generales del lenguaje Tinto

 

Objetivos

 

  • Presentar el lenguaje de programación Tinto
  • Describir el proceso de compilación y ejecución de programas en Tinto
  • Presentar el procesador MIPS
  • Presentar el simulador QT-SPIM

 

Código de la práctica

 

 

Características generales del lenguaje de programación Tinto

 

Tinto es un lenguaje de programación imperativo orientado a procesos (tipo C). El objetivo de Tinto es definir un lenguaje de programación muy simple que sirva de apoyo a la docencia de la asignatura de Procesadores de Lenguaje. En cada uno de los aspectos de diseño de Tinto (especificación léxica, sintáctica y semántica, tipos de datos, instrucciones, etc.) se ha optado por simplificar el lenguaje de forma que sea posible afrontar el desarrollo de un compilador tanto de forma manual como con la ayuda de herramientas. De esta forma, el lenguaje Tinto servirá como ejemplo en las sesiones prácticas en las que se explica cómo desarrollar un analizador léxico, un analizador sintáctico, etcétera.

A continuación se muestra un ejemplo del contenido de un fichero fuente descrito en lenguaje Tinto:

import Math;

library Ejemplo {

  public int MaximoComunDivisor(int a, int b) {

    int mcd = Math.min(a,b);

    while(mcd>0) {

       if( a%mcd == 0 && b%mcd == 0) return mcd;

       mcd = mcd - 1;

    }

    return 1;

  }

}

Algunas de las características de Tinto son las siguientes:

  • Palabras reservadas:

Tinto incluye un número reducido de palabras reservadas (15 en total). Comparado con C (32 palabras reservadas), C++ (50 palabras reservadas) o Java (53 palabras reservadas) se puede valorar el esfuerzo de simplificación realizado en el diseño del lenguaje. Las palabras reservadas de Tinto son las siguientes: boolean, char, else, false, if, import, int, library, native, private, public, return, true, void y while.

  • Tipos de datos:

El lenguaje incluye tan solo tres tipos de datos: int, char y boolean. Se asume que todos ellos serán almacenados en registros de 32 bits. El tipo char se considera descrito en código ASCII de 8 bits almacenados en el byte menos significativo (los 24 bits más significativos deben tener el valor 0). El tipo boolean utiliza el valor 0 para indicar falso y el valor 1 para indicar verdadero. Se considera también el tipo void para indicar que una función no devuelve ningún dato.

  • Instrucciones:

Las funciones de Tinto están formadas por una lista de instrucciones. Las instrucciones incluidas en el lenguaje son: declaraciones de variables locales (se admite la asignación en el momento de la declaración y la declaración conjunta de una lista de variables del mismo tipo), instrucciones de asignación simple (por medio del operador = ), instrucciones de ejecución de una función, instrucciones condicionales (if-else), bucles (while) y finalización de las funciones (return).

  • Operadores aritméticos y lógicos:

Tinto incluye los siguientes operadores aritméticos: suma ( + ), resta ( - ), multiplicación ( * ), división ( / ) y módulo ( % ).

Por su parte, los operadores lógicos incluidos en Tinto son los siguientes: and con cortocircuito ( && ), or con cortocircuito ( || ) y not ( ! ).

  • Operadores relacionales:

Los operadores relacionales permiten establecer comparaciones entre los valores de distintas expresiones. Tinto incluye cuatro operadores relacionales que pueden aplicarse a expresiones de tipo int y char: mayor ( > ), menor ( < ), mayor o igual ( >= ) y menor o igual ( <= ). Además se incluyen los operadores igual ( == ) y distinto ( != ) que son válidos para todos los tipos de datos (int, char y boolean).

  • Literales:

Tinto permite definir literales para cada tipo de datos: valores de tipo int (en formato decimal, octal, hexadecimal y binario), valores de tipo char (entre comillas simples que pueden incluir caracteres imprimibles, caracteres de escape y caracteres en formato octal) y valores de tipo boolean (true o false).

  • Componentes del lenguaje:

Los ficheros descritos en el lenguaje Tinto definen bibliotecas de funciones. Cada fichero contiene una lista de bibliotecas importadas (cláusulas import) y una biblioteca (library) cuyo nombre debe coincidir con el nombre del fichero. La biblioteca está formada por una lista de funciones que pueden ser públicas o privadas. El código de las funciones puede contener llamadas a otras funciones públicas o privadas de la misma biblioteca. También pueden contener llamadas a funciones públicas de otras bibliotecas. En este último caso, para identificar a la función llamada se utiliza el nombre de la biblioteca, seguido de un punto y del nombre de la función. Para poder utilizar las funciones de una biblioteca, ésta debe haber sido importada previamente en el fichero. El lenguaje permite definir también bibliotecas nativas (native) que están formadas por una lista de declaraciones de funciones. En este caso, el código de las funciones está desarrollado directamente en ensamblador y no se describe en el fichero fuente. Gracias a estas bibliotecas nativas se pueden utilizar funciones que usan instrucciones de ensamblador específicas que no genera el compilador, como llamadas al sistema (por medio de la instrucción syscall).

 

 

Proceso de compilación y ejecución de programas en Tinto

 

El compilador de Tinto se encuentra en el archivo tintoc.jar. Para realizar un proceso de compilación se debe ejecutar este archivo desde una línea de comandos:

java -jar tintoc.jar options

El objetivo del compilador es compilar el archivo Main.tinto que debe contener la función Main() que marca el comienzo de la aplicación programada. A partir de esta biblioteca se analizan y compilan el resto de bibliotecas importadas. Por defecto el directorio de trabajo es desde aquel en el que se ejecuta el comando de compilación, pero se puede modificar por medio de las opciones. Por defecto las bibliotecas importadas deben encontrarse en el directorio de trabajo pero se pueden configurar otros directorios de búsqueda por medio de la opción -I. El resultado de la compilación es un archivo con extensión ".s" con el código ensamblador que desarrolla la aplicación programada. Por defecto, el archivo generado como salida se denomina "Application.s" pero este nombre se puede modificar por medio de la opción -o. Por medio de la opción -v se le puede indicar al compilador que genere archivos con la descripción del código intermedio en modo texto. Las opciones que admite el compilador son las siguientes:

  • Path, indica directorio de trabajo.
  • -o Name, indica el nombre del fichero de salida (sin la extensión ".s").
  • -I Path, añade un directorio de búsqueda de archivos importados.
  • -v, indica que se generen los ficheros de código intermedio (por defecto no se generarán).

La distribución del compilador de Tinto contiene algunos ejemplos y un directorio llamado Utils que incluye una biblioteca llamada Console. Esta biblioteca contiene varios métodos de presentación de información en la consola. Para utilizar correctamente esta biblioteca es aconsejable añadir el directorio Utils como directorio de búsqueda por medio de la opción -I. El resultado de la compilación es un fichero escrito en ensamblador del procesador MIPS. Para ejecutar este código se utiliza el simulador Qt-Spim.

 

 

Características del procesador MIPS

 

MIPS (Microprocessor without Interlocked Pipeline Stages) es el nombre de una familia de procesadores RISC desarrollados por MIPS Technologies. La empresa concede licencias a otros fabricantes para integrar la arquitectura MIPS en sus productos (por ejemplo, CISCO, SGI, Toshiba, Sony, ...). La arquitectura MIPS es la base de los procesadores de muchos routers de CISCO, de las estaciones de trabajo de SGI, de las videoconsolas Nintendo 64, PlayStation, PlayStation 2, PlayStation Portable, de las impresoras de HP, etc.

Algunas de las características más interesantes de la arquitectura MIPS son las siguientes:

  • Describe procesadores RISC, cuyo código es más fácil de generar para un compilador.

  • Está muy bien documentada (Arquitectura) (Conjunto de instrucciones) (Arquitectura de recursos privilegiados)

  • Es una arquitectura de 32/64 bits.

  • Contiene una unidad en coma flotante que admite los formatos IEEE Standar 754 de 32 bits (float) y de 64 bits (double).

  • Podemos simular el código ensamblador por medio del simulador QT-SPIM.

La arquitectura cuenta con una unidad de propósito generar formada por 32 registros de 32 bits ($0 a $31) y una unidad de coma flotante con 32 registros de 32 bits ($f0 a $f31) que se pueden utilizar como 16 registros de 64 bits para almacenar datos en formato double.

Arquitectura de MIPS

Los registros de propósito general pueden usarse libremente en las instrucciones, aunque hay que tener en cuenta que el registro $0 está cableado a 0 y que el registro $31 almacena la dirección de retorno cuando se ejecuta una instrucción JAL (jump and link). El resto de registros se puede utilizar libremente aunque se recomienda seguir un convenio de uso de los registros que indica el tipo de dato que se debe almacenar en cada registro. Este convenio incluye utilizar una serie de alias para referirse a los registros. La siguiente tabla describe el convenio de uso de los registros de MIPS.

Registros de MIPS

El conjunto de instrucciones es sencillo y potente (Instrucciones CPU) (Instrucciones FPU) (Tabla resumen). La descripción completa de cada instrucción se encuentra aquí.

 

 

El simulador Qt-Spim

 

SPIM es un simulador que ejecuta programas descritos en ensamblador de MIPS32. La herramienta ha sido programada por James Larus y se distribuye libremente. La versión más reciente se denomina QT-SPIM y se puede descargar desde la página oficial.

El simulador desarrolla un conjunto mínimo de llamadas al sistema que permiten acceder a una consola y acceder a ficheros. La tabla de llamadas al sistema es ésta.

La ventana principal de Qt-Spim muestra tres paneles: el panel de la iquierda muestra el contenido de los registros del procesador, el panel de la derecha muestra el contenido de la memoria y el panel inferior se utiliza para mostrar mensajes. El panel de registros tiene dos pestañas que permiten seleccionar entre los registros de propósito general (GPR) y los registros de la unidad de coma flotante (FPR). El panel de memoria tiene también dos pestañas que permiten mostrar el segmento de texto (que contiene las instrucciones del programa a ejecutar) y el segmento de datos (que contiene la pila y la memoria estática y dinámica).

QtSpim

Para ejecutar un programa (el fichero Application.s, en nuestro caso) en primer lugar hay que cargarlo (Opción File->Load file). A continuación se puede ejecutar completamente (F5) o paso a paso (F10). También es posible establecer puntos de parada (breakpoints) y ejecutar el programa a saltos.

El proceso de ejecución se configura por medio de la opción (Simulator -> Settings). En nuestro caso, las opciones "Enable Delayed Branches" y "Enable Delayed Loads" deben estar marcadas para simular el procesador de forma realista. La opción "Accept Pseudo Instructions" tambien debe estar marcadas, ya que el ensamblador generado por el compilador de Tinto utiliza algunas pseudo-instrucciones. Es importante desmarcar la opción "Load Exception Handler" ya que nuestro fichero Application.s ya contiene el código de las excepciones.

QtSpim

El simulador contiene una consola en la que se pueden mostrar e introducir datos.

QtSpim

 

 

Ejercicios

 

  • Descargar e instalar el simulador QT-SPIM.
  • Descargar el código de la práctica y descomprimirlo en un directorio.
  • Ejecutar y comprobar los resultados de los ejercicios de ejemplo incluidos en el directorio examples.
  • Escribir un programa que devuelva el número de la posición 5 de la sucesión de Fibonacci.Compilarlo, ejecutarlo y comprobar que es correcto.