23 abril 2008

Un compilador sencillo paso a paso (8 - Mejorando el analizador léxico)

(Tras una -larga- pausa por motivos familiares, retomemos el compilador...)

Nuestro analizador léxico es capaz de leer un cierto símbolo del código fuente, de obtener un identificador o un número entero. Es la base de lo que necesitamos, pero aún tiene carencias. Dos de ellas son especialmente graves:


  • Es habitual que los identificadores permitan usar letras y números (por ejemplo, "variable01" debería ser un nombre correcto para una variable), con la restricción de que el primer símbolo sea una letra, no un número. Nuestro analizador por ahora sólo permite que estén formados por letras.


  • No permite usar comentarios en el código fuente.



El primer cambio, en la forma de definir los identificadores, es sencillo, apenas una modificación pequeña en la función "obtenerIdentificador":


letra := obtenerLetra;
lexema := lexema + letra;
letra := obtenerLetra;
(* Puede seguir con letras o numeros *)
while upcase(letra) in ['A'..'Z','0'..'9'] do
begin
lexema := lexema + letra;
letra := obtenerLetra;
end;


El segundo cambio tampoco es mucho más difícil: si un comentario empieza en llave debemos saltar todo hasta encontrar una llave cerrada (este cambio será parte de "saltarBlancos"), así:


(* Salto comentarios entre llaves *)
if (lineaDeEntrada[posicionLineaActual+1] = '{') then
begin
while (lineaDeEntrada[posicionLineaActual+1] <> '}') do
obtenerLetra;
obtenerLetra; (* Salto la llave de cierre *)
end;


Y de forma similar, saltaríamos los comentarios entre (* y *):


(* Salto comentarios entre parentesis-asterisco *)
if (lineaDeEntrada[posicionLineaActual+1] = '(')
and (lineaDeEntrada[posicionLineaActual+2] = '*')then
begin
(* Salto el parentesis y asterisco de apertura *)
obtenerLetra;
obtenerLetra;
while (lineaDeEntrada[posicionLineaActual+1] <> '*')
and (lineaDeEntrada[posicionLineaActual+2] <> ')') do
begin
obtenerLetra;
end;
obtenerLetra; (* Salto el asterisco y parentesis de cierre *)
obtenerLetra;



Por supuesto, quedan muchas cosas por hacer. Nuestro analizador empieza a reconocer correctamente los programas que no tengan fallos, pero es muy torpe en el manejo de errores. Es habitual crear un tipo de datos llamado "Token", de forma que de cada elemento que leamos (ya sea identificador, número entero, símbolo matemático, etc) sepamos otros datos, como la posición del fichero en que se encuentra (para que los mensajes de error sean más correctos), el tipo de elemento del que se trata, su valor, etc. Lo básico que necesitaríamos sería algo como:


type token =
record
tipoToken: integer;
valor: string;
posx,posy: integer;
end;


Aun así, esto son mejoras sin las cuales nuestro compilador puede funcionar "relativamente bien" al nivel que nos interesa por ahora, es decir, si nosotros somos cuidadosos tecleando los fuentes, porque los mensajes de error serán muy poco aclaradores.

En la próxima entrega mejoraremos nuestra tabla de símbolos y empezaremos a usar variables, cuyo valor pueda realmente ser modificado durante el funcionamiento del programa.

Y posiblemente algún día completaremos el analizador léxico para que sea más fiable... posiblemente... pero, en principio, esas mejoras las haré directamente en la página del proyecto en Google Code, no aquí.

08 abril 2008

Un compilador sencillo paso a paso (7 - Primera tabla de símbolos)

Los programas que reconoce nuestro compilador pueden tener instrucciones con o sin parámetros, pero hasta ahora esos parámetros sólo pueden ser números. En el "mundo real" esto no es así, es muy frecuente necesitar trabajar con valores cambiantes, que se almacenarán en variables.

Como primer acercamiento, vamos a hacer que nuestro compilador sea capaz de reconocer constantes, y en la próxima entrega ampliaremos para que también sean variables.

Existen distintos tipos de datos. Como nuestra máquina de destino tiene un procesador de 8 bits, por ahora usaremos el más sencillo para nosotros: un tipo de datos Byte, que ocupe 1 byte en memoria y pueda almacenar un número entero con un valor entre 0 y 255. Es algo limitado, pero que nos permitirá hacer proyectos pequeños.

De cada constante necesitaremos varios datos: el nombre, el tipo de datos (por ahora sólo "byte") y su valor. Más adelante necesitaremos otros datos parecidos de cada variable (pero no guardaremos su valor actual, sino la dirección de memoria en que éste se encuentra) y de cada función.

Permitiremos que se pueda declarar varias constantes, separadas por comas. Todas tendrán la forma "nombre = valor", comenzarán con la palabra "const" y terminarán con un punto y coma, de modo que un program sencillo podría ser así:


program ej;
const linea = 10, columnaA = 10, columnaB = 1;
begin
cpcMode(0);
pen(2);
paper(1);
locate(columnaA, linea);
writeChar( 'a' );
pen(4);
paper(3);
locate ( columnaB , linea );
writeChar('c');
end.


Cuando el analizador sintáctico encuentre la palabra "const", deberá guardar información sobre la constante que se le indique (o las constantes, si son varias). Para ello, dentro del generador de código crearemos una función "insertarSimboloConst", que recibirá dos parámetros: el nombre y el valor, y una segunda función "leerSimboloConst", que recibirá el nombre de la constante y devolverá su valor. Nuestra tabla de símbolos será rudimentaria: para no perder tiempo en crear estructuras de datos más eficientes, será simplemente un "array" de registros. Por tanto, para insertar un símbolo, recorreremos este array, añadiendo al final, o dando un mensaje de error si el símbolo está repetido:


(* Guardar una constante en la tabla de símbolos *)
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
begin
writeln('Constante repetida!');
halt;
end;
(* Si no existe, lo insertamos *)
numSimbolos := numSimbolos + 1;
listaSimbolos[numSimbolos].nombre := nombreConst;
listaSimbolos[numSimbolos].tipoDato := tBYTE;
listaSimbolos[numSimbolos].valorDir := valorConst;
end;



Para leer un símbolo de nuestra tabla de símbolos sencilla, recorremos el array, y damos un mensaje en caso de que no exista:


(* Leer una constante de la tabla de símbolos *)
function leerSimboloConst(nombreConst: string): byte;
var
encontrado: boolean;
i: integer;
x: byte;
codError: integer;
begin
nombreConst := upcase(nombreConst);
encontrado := false;
(* Primero comprobamos si el simbolo ya existe *)
for i:=1 to numSimbolos do
begin
if listaSimbolos[i].nombre = nombreConst then
begin
val(listaSimbolos[i].valorDir, x, codError);
leerSimboloConst := x;
encontrado := true;
end;
end;
(* Si no existe, mensaje de error *)
if encontrado = false then
begin
writeln('Constante inexistente!');
halt;
end;
end;



Ahora deberíamos modificar ligeramente las funciones que antes esperaban un parámetro numérico, como CpcMode, para que esta vez reciban un identificador o un número, si se trata de un identificador, deberemos buscar su valor en la tabla de símbolos. Lo podríamos hacer así:


procedure analizarCPCMODE;
begin
leerSimbolo('(');

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

leerSimbolo(')');
leerSimbolo(';');
genCPCMODE(x);
end;


Con pocos cambios similares a ese, conseguiríamos que nuestro mini-compilador reconozca tanto valores numéricos como constantes.

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/

04 abril 2008

Un compilador sencillo paso a paso (6 - Nuevas órdenes)

Sigamos ampliando nuestro compilador elemental. Nos queda mucho por hacer. Algunas cosas serán relativamente trabajosas, como declarar y usar variables, o más todavía cuando queramos crear procedimientos, pero algún otro detalle es sencillo, como seguir añadiendo nuevas órdenes simples.

Comenzaremos por ahí: vamos a añadir tres órdenes nuevas, que permitan cambiar el modo de pantalla, el color del texto y el color de fondo.


  • La orden de cambiar un modo de pantalla será "CpcMode", que recibirá un único parámetro: el número de modo (0 para 20 columnas y 16 colores, 1 para 40 columnas y 4 colores, 2 para 80 columnas y 2 colores). Le pondremos el prefijo "Cpc" por eso de que es una orden "casi exclusiva de los CPC", mientras que las anteriores (cls, locate, writechar) tendrían sentido en casi cualquier sistema informático.


  • Las órdenes de cambiar color, como también existirán para casi cualquier sistema, no tendrán el prefijo "cpc", y seguirán la nomenclatura que se usaba en el Basic de estos equipos: PEN para cambiar el color de escritura (pluma) y PAPER para el color de fondo (papel).



Así, al generador de código (uPachiG) bastaría con añadirle las correspondientes llamadas a rutinas del firmware:


(* Generar codigo para CPCMODE *)
procedure genCPCMODE(modo: byte);
begin
writeln( ficheroDestino, lineaActual,' DATA 3E,',
hexStr(modo,2), ': '' LD A, ',modo );
lineaActual := lineaActual + 10;
writeln( ficheroDestino, lineaActual,' DATA CD,0E,BC: '' CALL &BC0E - SET MODE' );
lineaActual := lineaActual + 10;
longitudTotal := longitudTotal + 5;
end;

(* Generar codigo para PAPER *)
procedure genPAPER(color: byte);
begin
writeln( ficheroDestino, lineaActual,' DATA 3E,',
hexStr(color,2), ': '' LD A, ', color);
lineaActual := lineaActual + 10;
writeln( ficheroDestino, lineaActual,' DATA CD,96,BB: '' CALL &BB96 - SET PAPER' );
lineaActual := lineaActual + 10;
longitudTotal := longitudTotal + 5;
end;

(* Generar codigo para PEN *)
procedure genPEN(color: byte);
begin
writeln( ficheroDestino, lineaActual,' DATA 3E,',
hexStr(color,2), ': '' LD A, ', color);
lineaActual := lineaActual + 10;
writeln( ficheroDestino, lineaActual,' DATA CD,90,BB: '' CALL &BB90 - SET PEN' );
lineaActual := lineaActual + 10;
longitudTotal := longitudTotal + 5;
end;




En el analizador sintáctico, los cambios son pequeños también. Aun así, podemos aprovechar para hacerlo un poco más modular: los detalles del análisis no deberían estar todos dentro del procedimiento "analizarCualquierOrden", sino que éste debería delegar en otros, así:


procedure analizarCualquierOrden;
begin
orden := upcase(obtenerIdentificador);
if orden = 'CLS' then
begin
analizarCLS;
end
else
if orden = 'CPCMODE' then
begin
analizarCPCMODE;
end
else

[...]

procedure analizarCLS;
begin
leerSimbolo(';');
genCLS;
end;

procedure analizarCPCMODE;
begin
leerSimbolo('(');
val(obtenerEntero,x,codError);
leerSimbolo(')');
leerSimbolo(';');
genCPCMODE(x);
end;

procedure analizarPAPER;
begin
leerSimbolo('(');
val(obtenerEntero,x,codError);
leerSimbolo(')');
leerSimbolo(';');
genPAPER(x);
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/