06 agosto 2008

Un compilador sencillo paso a paso (10 - Variables utilizables)

Tras un paréntesis forzoso durante julio por exceso de trabajo y de compromisos familiares, vamos a retomar el proyecto, a ver si hay suerte y durante agosto queda terminado un compilador razonablemente utilizable...

En el acercamiento anterior habíamos comentado alguna de las ideas básicas del manejo de variables, y habíamos un primer acercamiento al analizador que reconozca la declaración de variables, pero todavía no sabíamos manipularlas. Eso es lo que haremos ahora.

Tendremos que ser capaces de hacer cosas como:

  • Dar un valor a una variable: a := 1

  • Cambiar el valor de una variable. La primera forma que haremos, porque no supone analizar expresiones aritméticas, podría ser la operación "incremento": inc(a).

  • Usar el valor de una variable, con operaciones como cpcMode(a).



Como las variables se almacenan en operaciones de memoria, todas esas operaciones supondrán leer el valor de la memoria, y, a veces, modificarlo y volverlo a guardar. En concreto, si suponemos que la variable "a" está en la posición de memoria 10.000, esas operaciones se podrían convertir a las siguientes secuencias en ensamblador:

a := 1

ld a, 1 ; Cargar el valor 1 en el acumulador
ld (10000), a ; Guardar en la posición 10.000 el valor del acumulador


inc(a)
ld a, (10000) ; Guardar en el acumulador el valor guardado en la posición 10.000
inc a ; Incrementar el valor del acumulador
ld (10000), a ; Guardar en la posición 10.000 el valor del acumulador


cpcMode(a)
ld a, (10000) ; Guardar en el acumulador el valor guardado en la posición 10.000
call &bc0e ; Cambiar modo de pantalla


Esto supone varios cambios en el fuente: para instrucciones como INC bastará con añadir nuevas rutinas de generación de código, pero para órdenes existentes como cpcMode ahora deberemos comprobar si el parámetro es una constante (y en ese caso haríamos cosas como LD A,1) o si es una variable (y entonces tomaremos el valor desde una posición de memoria: LD A, (10000) ).

Pero también hay otro cambio grande: ¿cómo asignamos las direcciones de memoria para esas variables?  ¿Y dónde dejamos el espacio para ellas?

Hay varias soluciones:

  • La primera solución, la más eficiente en cuanto a espacio, sería dejar el espacio para las variables al final del programa. Pero esta solución tiene un problema: hasta que no terminamos de generar el programa, no sabemos cuánto ocupa, de modo que no hasta entonces no sabríamos las direcciones de las variables, por lo que deberíamos dar dos pasadas: en la primera pasada se genera todo el programa excepto las direcciones de las variables (se suele dejar indicado con una etiqueta, algo como "LD A, [variableX]"), y en la segunda pasada será cuando se escriban realmente las posiciones de memoria en lugar de estas etiquetas.

  • La segunda solución es dejar el espacio para variables al principio del programa. Ahora el inconveniente será parecido: como no sabemos cuantas variables hay en el programa, no tendremos claro en qué posición de memoria empezarán las órdenes... hasta que no demos una primera pasada. Además, existe un segundo riesgo: antes lanzábamos nuestro programa resultante con algo como CALL 40000, pero ahora la primera orden ya no está en esa posición, sino en otra, así que deberemos recordar en cual, para que el cargador sea adecuado (por ejemplo, CALL 40015).

  • Nosotros queremos esquivar (todavía) las dos pasadas, e intentar hacerlo todo en una pasada. Así que la aproximación que haremos es dar por sentado que "algo" ocupa una cierta cantidad de bytes como máximo. Por ejemplo, podríamos suponer que el programa empieza en la posición de memoria 30.000, y que no ocupa más de 10.000 bytes, por lo que las variables podrían estar en la posición de memoria 40.000. Como el tamaño de un programa es algo muy difícil de prever, y la cantidad de variables no lo es tanto, quizá fuera más razonable decir que las variables empiezan en la posición 30.000 (por ejemplo) y el programa en la 30.200. No es una aproximación fiable, pero sí es sencilla, y todavía buscamos sobre todo sencillez. Para evitar "despistes" en los que un CALL 30000 no accediera al programa sino al comienzo de los datos, lo que podría tener resultados imprevisibles, nos reservaremos los primeros 3 bytes de datos, no para variables, sino para almacenar una orden de salto a la posición 30200 (o a la que hayamos decidido que será la de comienzo del programa), o bien podemos obligarnos a rellenar con 0 (NOP) toda esa
    zona de variables inicialmente.



Vamos a ver los cambios que todo esto supondría al código del compilador:

En primer lugar, en la tabla de símbolos podremos guardar constantes y variables, luego podemos añadir un dato "constVar" que sea el que indique si un símbolo es una constante (cuyo valor podremos leer directamente en la tabla, pero no modificar) o una variable (para la que guardaremos la dirección de memoria, y el valor obtenido se deberá poder modificar). Por tanto, el registro de cada símbolo podría ser:


type simbolo =
record
nombre: string; (* Nombre del simbolo *)
constVar: integer; (* Si es CONST o VAR *)
tipoDato: integer; (* Tipo base: BYTE, etc *)
valorDir: string; (* Valor o direccion *)
end;


Para ese dato "constVar" que indica si es una constante o una variable, podemos definir dos constantes que nos ayuden a mantener la legibilidad del código:


const
tCONST = 1; (* Dato CONST en tabla de simbolos *)
tVAR = 2; (* Dato VAR en tabla de simbolos *)


Ahora la rutina de insertar una constante deberá tener ese dato en cuenta:


(* Guardar una constante en la tabla de simbolos *)
procedure insertarSimboloConst(nombreConst: string; valorConst: string);
var
i: integer;
begin
nombreConst := upcase(nombreConst);
(* Primero comprobamos si el simbolo ya existe *)
for i:=1 to numSimbolos do
if listaSimbolos[i].nombre = nombreConst then
errorFatal('Constante repetida!');
(* Si no existe, lo insertamos *)
numSimbolos := numSimbolos + 1;
listaSimbolos[numSimbolos].nombre := nombreConst;
listaSimbolos[numSimbolos].constVar := tCONST;
listaSimbolos[numSimbolos].tipoDato := tBYTE;
listaSimbolos[numSimbolos].valorDir := valorConst;
end;


La rutina de insertar una variable será parecida, salvo porque no tenemos un valor, sino una dirección. Además, esta dirección no la deberíamos fijar nosotros a mano, sino que se debería ir recalculando a medida que guardemos variables. Así, podríamos delegar en una función auxiliar "proximaDireccionLibre", a la que además le podemos indicar el tamaño del dato, de forma que sea algo más eficiente cuando nuestras variables no sean sólo de tipo byte:


(* Guardar una variable en la tabla de simbolos *)
procedure insertarSimboloVar(nombreVar: string; tipoVar: char);
var
i: integer;
begin
nombreVar := upcase(nombreVar);
(* Primero comprobamos si el simbolo ya existe *)
for i:=1 to numSimbolos do
if listaSimbolos[i].nombre = nombreVar then
errorFatal('Identificador repetido!');
(* Si no existe, lo insertamos *)
numSimbolos := numSimbolos + 1;
listaSimbolos[numSimbolos].nombre := nombreVar;
if tipoVar = 'b' then
listaSimbolos[numSimbolos].tipoDato := tBYTE
else
begin
writeln('Tipo de datos desconocido!');
halt;
end;
listaSimbolos[numSimbolos].constVar := tVAR;
(* Direccion, en vez de valor *)
listaSimbolos[numSimbolos].valorDir := proximaDireccionLibre(tByte);
end;


Finalmente, ahora las funciones podrán tener como parámetros constantes
numéricas, constantes con valor, o variables, por lo que su análisis "previo" será algo más trabajoso que antes, así que podríamos crear una rutina específica que se encargue de ello. De este modo, rutinas como la de analizar la orden PEN, que antes era así:


procedure analizarPEN;
begin
leerSimbolo('(');

cadenaTemp := obtenerEnteroOIdentificador;
if cadenaTemp[1] in ['a'..'z'] then
x := leerSimboloConst(cadenaTemp)
else
val(cadenaTemp,x,codError);

leerSimbolo(')');
leerSimbolo(';');
genPEN(x);
end;


Pasarán a ser simplemente así:


procedure analizarPEN;
begin
leerSimbolo('(');
genLecturaValor(obtenerEnteroOIdentificador);
leerSimbolo(')');
leerSimbolo(';');
genPEN(x);
end;


Y ese procedimiento encargado de generar la orden de lectura del valor según se trate de una constante numérica, una constante con nombre o una variable podría ser así:


procedure genLecturaValor(nombre: string);
var
valorByte: byte;
direccion: integer;
begin
if nombre[1] in ['0'..'9'] then
begin
(* Si es numero *)
val(nombre,valorByte,codError);
writeln( ficheroDestino, lineaActual,' DATA 3E,',
hexStr(valorByte,2), ': '' LD A, ',valorByte );
lineaActual := lineaActual + 10;
longitudTotal := longitudTotal + 2;
end
else
if leerSimboloTS(nombre) = tCONST then
(* Si es constante con nombre *)
begin
valorByte := leerSimboloConst(nombre);
writeln( ficheroDestino, lineaActual,' DATA 3E,',
hexStr(valorByte,2), ': '' LD A, ',valorByte, ' - Constante ',nombre );
lineaActual := lineaActual + 10;
longitudTotal := longitudTotal + 2;
end
else
(* Si es variable *)
begin
direccion := leerSimboloVar(nombre);
writeln( ficheroDestino, lineaActual,' DATA 3A,',
hexStr(direccion mod 256,2), ',',
hexStr(direccion div 256,2),
': '' LD A, (',direccion, ') - Variable ',nombre );
lineaActual := lineaActual + 10;
longitudTotal := longitudTotal + 3;
end;
end;



Como siempre, para más detalles, todo el código está en la página del proyecto en Google Code:

http://code.google.com/p/cpcpachi/