23 agosto 2008

Un compilador sencillo paso a paso (14 - Acercamiento a los procedimientos)

En los lenguajes estructurados como Pascal se pueden definir procedimientos, que "encapsulan" una serie de pasos, y funciones, que además devuelven un valor. Nuestro compilador aún tiene muchas carencias (no se pueden declarar dos variables a la vez, no existen distintos tipos de datos, faltan muchas órdenes por incluir...) y más de un error (por ejemplo, no es capaz de reconocer operadores formados por dos símbolos, como <=), pero aun así vamos a acercarnos un poco a cómo se podría generar el código para los procedimientos, y en el próximo paso será cuando vayamos limando asperezas para que nuestro compilador, además de hacer un poco de todo, funcione un poco mejor...

En una primera toma de contacto, queremos que nuestro compilador sea capaz de generar el código para procedimientos muy básicos: sin parámetros y sin variables locales (y, al no tratarse de funciones, tampoco devolverán ningún valor).

Pretendemos que sea capaz de traducir correctamente fragmentos como

procedure inicializa;
begin
cpcMode(1);
paper(0);
end;


Que luego se usarían desde el cuerpo del programa:

begin
inicializa;
locate(10,3);
...


Esto supone una serie de problemas:

  • El primer código que encontramos al analizar el programa no será siempre realmente el cuerpo del programa, sino que puede tratarse de un procedimiento que será llamado más adelante... o quizá incluso nunca.

  • Cada procedimiento podrá ser llamado desde el cuerpo del programa o desde otro procedimiento, y cuando termine su ejecución deberá volver al punto siguiente del cuerpo del programa (o del otro procedimiento).



Podemos intentar resolverlo así:

  • Podríamos incluir el código de los procedimientos al final, después del código del cuerpo del programa, pero si queremos hacer todo el análisis en una pasada no es la mejor solución. La alternativa para poder ir convirtiendo a medida que se va leyendo es generar el código de los procedimientos según se encuentren, es decir, antes del programa. Por eso, como es posible que lo primero que encontremos no sea lo primero que debemos ejecutar, el programa debería comenzar con un salto al comienzo del cuerpo (que aún no sabemos dónde será): JP INICIO_PROGRAMA.

  • Cada procedimiento terminará con un RET, para volver al punto desde el que se llamó, y tendrá una dirección de comienzo, a la que llamaremos con un CALL.



Así, el programa convertido a ensamblador tendría una apariencia similar a:

JP INICIO_PROGRAMA
INICIO_INICIALIZA
...
RET
INICIO_LINEA_HORIZONTAL
...
RET
INICIO_PROGRAMA
...


La situación se complica si queremos permitir variables locales y/o parámetros. No lo vamos a hacer todavía, pero sí a ver las nociones básicas, para aplicarlas más adelante:

  • El hecho de que haya variables locales, y de que alguna de ellas pueda llamarse igual que una variable global, se puede solucionar añadiendo información adicional en la tabla de símbolos: el ámbito al que pertenece una variable. Por ejemplo, para una variable "i" en el procedimiento "inicializa" podría haber otro campo que indique el procedimiento al que pertenece, o podríamos simplemente cambiar su nombre por "i_inicializa" durante el análisis, pero eso fallaría si el procedimiento (o función) es recursivo. También deberíamos plantearnos declarar la variable cuando se entra al procedimiento y eliminar la declaración al salir de él, para no desperdiciar espacio que quizá no se esté usando.

  • Sobre los parámetros, la forma habitual de trabajar es preparar un "registro de activación", que contiene detalles sobre los parámetros, y quizá incluso sobre datos locales e "información administrativa", como la dirección a la que se debe volver cuando se termine de analizar el procedimiento. De momento todo ello son datos que nosotros no vamos a usar.



De paso, aprovecharemos para hacer alguna pequeña mejora: que se puedan declarar varias variables del mismo tipo a la vez, algo que todavía no permitía nuestro analizador:

var
i, j, k: byte;