31 marzo 2008

Un compilador sencillo paso a paso (5)

En el quinto acercamiento vamos a reconocer una estructura algo más parecida a un fuente "real" en pascal, que comience con "program" y tenga un cuerpo de programa delimitado ente "begin" y "end", incluso con espacios en blanco, como ésta:


program ej;
begin
 cls;
 locate(10,5);
 writeChar( 'a' );
 locate ( 1 , 10 );
 writeChar('c');
end.


Esto supone dos cambios:


  • Vamos a sacar del cuerpo del programa lo que será el "analizador
    sintáctico", el encargado de indicar en qué orden deben estar los
    elementos del programa (por ejemplo: después de "program" debe aparecer
    un identificador y un punto y coma, luego "begin" y una secuencia
    de órdenes, etc).

  • Ampliaremos un poco el analizador léxico para que sea capaz
    de "saltarse" los espacios en blanco.



En cuanto al "analizador sintáctico", simplemente ampliamos el anterior "analizarCualquierOrden", que ahora estará dentro de un "analizarPrograma", que se asegura de que antes aparezca program, begin, etc, así:


procedure analizarPrograma;
begin
(* program <identificador>; *)
orden := upcase(obtenerIdentificador);
if orden <> 'PROGRAM' then
begin
writeln('No se ha encontrado PROGRAM');
halt;
end;
obtenerIdentificador;
leerSimbolo(';');

(* begin *)
orden := upcase(obtenerIdentificador);
if orden <> 'BEGIN' then
begin
writeln('No se ha encontrado BEGIN');
halt;
end;

(* Resto de ordenes *)
while not finFicheroEntrada do
begin
analizarCualquierOrden;
end;
end;



Por lo que respecta al analizador léxico, cada vez que vayamos a leer un identificador, un entero o a esperar un símbolo concreto, deberemos antes saltar los espacios en blanco que pudiera haber:


function obtenerIdentificador: string;
var
lexema: string;
letra: char;
begin
saltarBlancos;
lexema := '';
letra := obtenerLetra;
[...]

procedure saltarBlancos;
begin
while (lineaDeEntrada[posicionLineaActual+1] in espacioBlanco) do
obtenerLetra;
end;

espacioBlanco: set of char = [chr(32), chr(10), chr(13)];


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

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

27 marzo 2008

Un compilador sencillo paso a paso (4)

Vamos a por el cuarto acercamiento. Hemos hecho que la entrada sea un poco más flexible, con un primer analizador léxico. Ahora le toca el turno a la salida: la generación de código.

Si hacemos que el generador de código sea un módulo independiente (lo elegante sería que fuera un "objeto"; para nosotros será simplemente unas cuantas funciones recopiladas en una misma "unit"), conseguiremos varias ventajas:

  • Más legibilidad de lo que podríamos considerar el "código fuente principal".
  • Más facilidad para reemplazar esa "unit" por otra cuando queramos generar código para otra máquina de destino distinta.
Aun así, nuestra aproximación es pobre:
  • En un compilador "real" se suele generar primero un código intermedio, parecido al ensamblador (en ocasiones puede ser algo más "sofisticado", como un árbol sintáctico, que a nosotros ahora nos pilla bastante lejos).
  • A este código se le suele echar un primer vistazo (automático) para intentar optimizar las cosas que la generación de código ha hecho de forma mejorable.
  • A continuación se genera el código máquina.
  • Finalmente, se suele intentar optimizar también ese código máquina.

Claramente, nosotros no hacemos nada de eso (al menos por ahora): nos limitamos a generar código máquina en una sola pasada.

Conociendo nuestras limitaciones, buscamos que nuestro "código principal" (lo que dentro de poco llamaremos el analizador sintáctico) sea más simple y legible que antes, así:


program cpcPaChi;

(* Chi - Un compilador de Pascal chiquitito para CPC
Por Nacho Cabanes - Version 0.03

Versiones hasta la fecha:

Num. Fecha Cambios
---------------------------------------------------
0.04 27-Mar-2008 Primera version con generador de codigo
independiente (en la unit "uPaChiG")

0.03 25-Mar-2008 Creado un analizador lexico independiente
(en la unit "uPaChiL")

0.02 23-Mar-2008 Admite CLS, LOCATE, WRITECHAR,
que lee desde fichero. Solo permite
una orden en cada linea, con formato fijo

0.01 21-Mar-2008 Preliminar: solo acepta CLS por teclado
y genera el codigo correspondiente

*)

uses
upachil, (* Analizador lexico *)
upachig; (* Generador de codigo *)

var
nombreOrigen, nombreDestino: string;
orden: string;

(* Analiza las opciones que teclee el usuario. Por ahora,
solo el nombre del fichero a compilar *)
procedure analizarOpciones;
begin
if paramcount >= 1 then
nombreOrigen := paramstr(1)
else
begin
writeln('No se ha indicado el nombre del fichero');
halt;
end;
abrirFicheroEntrada( nombreOrigen );
abrirFicheroDestino( 'salida.bas' );
end;


(* Genera el codigo de destino: codigo maquina de Amstrad CPC
dentro de un cargador en Basic *)
procedure analizarUnaOrden;
var
x, y: byte;
letra: char;
codError: integer;
begin
orden := upcase(obtenerIdentificador);
if orden = 'CLS' then
begin
genCLS;
end
else
if orden = 'WRITECHAR' then
begin
leerSimbolo('(');
leerSimbolo('''');
letra := obtenerLetra;
leerSimbolo('''');
leerSimbolo(')');
genWRITECHAR(letra);
end
else
if orden = 'LOCATE' then
begin
leerSimbolo('(');
val(obtenerEntero,x,codError);
leerSimbolo(',');
val(obtenerEntero,y,codError);
leerSimbolo(')');
genLOCATE(x,y);
end
else
if orden = '' then
begin
(* orden vacia *)
end
else
begin
writeln('Error: orden no reconocida. Se esperaba: CLS, LOCATE o WRITECHAR');
(*halt;*)
end;

end;


(* Cuerpo del programa *)
begin
writeln('Compilando...');
analizarOpciones;
while not finFicheroEntrada do
begin
analizarUnaOrden;
end;
generarCodigoFinal;
cerrarFicheroEntrada;
cerrarFicheroDestino;
end.


Esto lo conseguiríamos con una analizador sintáctico como éste:


unit upachig;

(* cpcPaChi - Un compilador de Pascal chiquitito para CPC
Por Nacho Cabanes

uPachiG - unidad "CodeGen" (generador de codigo)

Versiones hasta la fecha:

Num. Fecha Cambios
---------------------------------------------------
0.04 27-Mar-2008 Primera version del generador de codigo:
genera codigo para CLS, LOCATE, WRITECHAR
y el codigo final del cargador BASIC
para un codigo maquina que estara dentro
de ordenes DATA.
*)


interface

function abrirFicheroDestino(nombre: string): boolean;
procedure cerrarFicheroDestino;

procedure genCLS;
procedure genLOCATE(x,y: byte);
procedure genWRITECHAR(letra: char);

procedure generarCodigoFinal;


implementation

const
lineaActual: integer = 10;
longitudTotal: integer = 0;


var
ficheroDestino: text;


(* Abre el fichero de entrada. Devuelve FALSE si no se
ha podido *)
function abrirFicheroDestino(nombre: string): boolean;
begin
assign( ficheroDestino, nombre );
{$I-}
rewrite( ficheroDestino );
{$I+}

if ioResult <> 0 then
begin
abrirFicheroDestino := false;
end;
end;


(* Generar codigo para CLS *)
procedure genCLS;
begin
writeln( ficheroDestino, lineaActual,' DATA CD,6C,BB: '' CALL &BB6C - CLS' );
lineaActual := lineaActual + 10;
longitudTotal := longitudTotal + 3;
end;

(* Generar codigo para LOCATE *)
procedure genLOCATE(x,y: byte);
begin
writeln( ficheroDestino, lineaActual,' DATA 3E,',
hexStr(x,2), ': '' LD A, ',x );
lineaActual := lineaActual + 10;
writeln( ficheroDestino, lineaActual,' DATA CD,6F,BB: '' CALL &BB6F - CURSOR COLUMN' );
lineaActual := lineaActual + 10;
writeln( ficheroDestino, lineaActual,' DATA 3E,',
hexStr(y,2), ': '' LD A, ',y );
lineaActual := lineaActual + 10;
writeln( ficheroDestino, lineaActual,' DATA CD,72,BB: '' CALL &BB72 - CURSOR ROW' );
lineaActual := lineaActual + 10;
longitudTotal := longitudTotal + 10;
end;


(* Generar codigo para WRITECHAR *)
procedure genWRITECHAR(letra: char);
begin
writeln( ficheroDestino, lineaActual,' DATA 3E,',
hexStr(ord(letra),2), ': '' LD A, "',letra, '"' );
lineaActual := lineaActual + 10;
writeln( ficheroDestino, lineaActual,' DATA CD,5A,BB: '' CALL &BB5A - WRITECHAR' );
lineaActual := lineaActual + 10;
longitudTotal := longitudTotal + 5;
end;

(* Cerrar fichero de destino (al final de la generacion) *)
procedure cerrarFicheroDestino;
begin
close(ficheroDestino);
end;


(* Genera el codigo de destino final: cargador Basic *)
procedure generarCodigoFinal;
begin
append( ficheroDestino );
writeln( ficheroDestino, lineaActual,' DATA C9: '' RET' );
writeln( ficheroDestino, lineaActual+10,' longitud = ',longitudTotal );
writeln( ficheroDestino, lineaActual+20,' MEMORY 39999' );
writeln( ficheroDestino, lineaActual+30,' FOR n=40000 TO 40000+longitud' );
writeln( ficheroDestino, lineaActual+40,' READ a$:POKE n,VAL("&"+a$)' );
writeln( ficheroDestino, lineaActual+50,' NEXT' );
writeln( ficheroDestino, lineaActual+60,' CALL 40000' );
end;


begin
end.


Por cierto... leer código fuente en un Blog no especialmente cómodo. Si quieres leer esta información con sintaxis coloreada en colores, o echar un vistazo más detenido al fuente, o descargarlo a partir de un fichero ZIP, puedes mirar la página del proyecto en Google Code:

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

25 marzo 2008

Un compilador sencillo paso a paso (3)

Este tercer acercamiento va a incluir un primer analizador léxico, que se encargue de extraer la información que le pidamos del fichero de entrada.



La intención es tener funciones como "obtenerIdentificador" o como "obtenerEntero", que simplifiquen el análisis del programa fuente.



Por ejemplo, el análisis de "LOCATE", que debe estar seguido por un paréntesis abierto, un número entero, una coma, otro número entero y un paréntesis cerrado, antes era totalmente artesanal:




(* Leo X: 1 o 2 letras *)
posicion := 8;
x := ord(orden[posicion])-ord('0');
posicion := posicion + 1;
if orden[posicion] ',' then
begin
x := x*10 + ord(orden[posicion])-ord('0');
posicion := posicion + 1;
end;
(* Tras la coma esta Y: 1 o 2 letras *)
posicion := posicion + 1;
y := ord(orden[posicion])-ord('0');
posicion := posicion + 1;
if orden[posicion] ')' then
begin
y := y*10 + ord(orden[posicion])-ord('0');
posicion := posicion + 1;
end;


y ahora será algo mucho más legible:




leerSimbolo('(');
val(obtenerEntero,x,codError);
leerSimbolo(',');
val(obtenerEntero,y,codError);
leerSimbolo(')');


Las posibilidades de nuestro analizador léxico son pocas por ahora, pero suficientes para lo que necesitamos en este momento:




  • Una función "obtenerLetra", que devuelve una letra (un "char") del fichero de entrada. Internamente se lee de linea en linea: cada letra se toma de la línea actual, o se pide una nueva línea si no quedan más letras en la actual.

  • Una función "obtenerIdentificador", que devuelve una cadena ("string") formada por un conjunto de letras de la A a la Z (en "el mundo real" se suelen permitir también cifras numéricas dentro de un identificador, pero a nosotros nos basta así por ahora).

  • Una función "obtenerEntero", que devuelve un número entero, como cadena ("string") formada por un conjunto de cifras del 0 al 9.

  • Una función "leerSimbolo", que intenta leer un cierto símbolo desde la entrada, y sale con un mensaje de error si no lo encuentra. Es nuestra primera ayuda para el análisis sintáctico, del que hablaremos pronto: ciertos símbolos deben aparecer en un cierto orden (por ejemplo, después de LOCATE debe hacer un paréntesis abierto, un número entero, una coma...) y esta función nos servirá para comprobar que es así.



El código fuente de este primer analizador léxico será




unit upachil;

(* cpcPaChi - Un compilador de Pascal chiquitito para CPC
Por Nacho Cabanes

uPachiL - unidad "Lexer" (analizador lexico)

Versiones hasta la fecha:

Num. Fecha Cambios
---------------------------------------------------
0.03 25-Mar-2008 Primera version del analizador lexico:
es capaz de devolver un identificador,
un entero y de comprobar si el siguiente
simbolo es uno prefijado;
lee linea a linea del fichero de entrada
*)


interface

function abrirFicheroEntrada(nombre: string): boolean;
function obtenerLetra: char;
function obtenerIdentificador: string;
function obtenerEntero: string;
procedure cerrarFicheroEntrada;
procedure leerSimbolo(simbolo:char);

var
lineaDeEntrada: string;
finFicheroEntrada: boolean;

implementation

var
ficheroEntrada: text;
posicionLineaActual: integer;


(* Abre el fichero de entrada. Devuelve FALSE si no se
ha podido *)
function abrirFicheroEntrada(nombre: string): boolean;
begin
assign( ficheroEntrada, nombre );
{$I-}
reset( ficheroEntrada );
{$I+}
posicionLineaActual := 0;

if ioResult = 0 then
begin
finFicheroEntrada := false;
abrirFicheroEntrada := true;
end
else
begin
finFicheroEntrada := true;
abrirFicheroEntrada := false;
end;
end;


(* Cerrar fichero de entrada (al final del analisis) *)
procedure cerrarFicheroEntrada;
begin
close(ficheroEntrada);
end;

(* Lee una linea del fichero de entrada. Solo para uso interno del lexer *)
procedure leerLinea;
begin
readln( ficheroEntrada, lineaDeEntrada );
if eof(ficheroEntrada) then
finFicheroEntrada := true;
end;

(* Obtiene una letra del fichero. Se apoya en leerLinea: toma la
siguiente letra de "LineaDeEntrada", o lee otra linea del fichero
si la actual ha terminado *)
function obtenerLetra: char;
begin
if posicionLineaActual >= length(lineaDeEntrada) then
begin
leerLinea;
posicionLineaActual := 0;
obtenerLetra := ' ';
end
else
begin
inc(posicionLineaActual);
obtenerLetra := lineaDeEntrada[posicionLineaActual];
end;
end;

(* Obtiene un identificador: devuelve una secuencia de letras *)
function obtenerIdentificador: string;
var
lexema: string;
letra: char;
begin
lexema := '';
letra := obtenerLetra;
while upcase(letra) in ['A'..'Z'] do
begin
lexema := lexema + letra;
letra := obtenerLetra;
end;
(* He leido uno de mas, asi que retrocedo *)
if posicionLineaActual > 0 then
dec(posicionLineaActual);
obtenerIdentificador := lexema;
end;


(* Obtiene un entero: devuelve una secuencia de cifras *)
function obtenerEntero: string;
var
lexema: string;
letra: char;
begin
lexema := '';
letra := obtenerLetra;
while upcase(letra) in ['0'..'9'] do
begin
lexema := lexema + letra;
letra := obtenerLetra;
end;
(* He leido uno de mas, asi que retrocedo *)
if posicionLineaActual > 0 then
dec(posicionLineaActual);
obtenerEntero := lexema;
end;


(* Obtiene un identificador: devuelve una secuencia de letras *)
procedure leerSimbolo(simbolo:char);
var
letra: char;
begin
letra := obtenerLetra;
if letra <> simbolo then
begin
writeln('Se esperaba ',simbolo,' y se ha encontrado ',letra);
halt;
end;
end;


begin
end.



Y el código fuente del programa principal será:




program cpcPaChi;


(* Chi - Un compilador de Pascal chiquitito para CPC
Por Nacho Cabanes - Version 0.03

Versiones hasta la fecha:

Num. Fecha Cambios
---------------------------------------------------
0.03 25-Mar-2008 Creado un analizador lexico independiente
(en la unit "uPaChiL")

0.02 23-Mar-2008 Admite CLS, LOCATE, WRITECHAR,
que lee desde fichero. Solo permite
una orden en cada linea, con formato fijo

0.01 21-Mar-2008 Preliminar: solo acepta CLS por teclado
y genera el codigo correspondiente

*)

uses upachil; (* Analizador lexico *)

const
lineaActual: integer = 10;
longitudTotal: integer = 0;

var
ficheroDestino: text;
nombreOrigen, nombreDestino: string;
orden: string;

(* Analiza las opciones que teclee el usuario. Por ahora,
solo el nombre del fichero a compilar *)
procedure analizarOpciones;
begin
if paramcount >= 1 then
nombreOrigen := paramstr(1)
else
begin
writeln('No se ha indicado el nombre del fichero');
halt;
end;
abrirFicheroEntrada( nombreOrigen );
nombreDestino := 'salida.bas';
assign( ficheroDestino, nombreDestino );
rewrite( ficheroDestino );
end;


(* Analiza la orden que el usuario ha dado, y sale con un mensaje
de error si es incorrecta *)
procedure analizarUnaOrden;
begin
orden := upcase(obtenerIdentificador);
if (orden <> 'CLS')
and (orden <> 'LOCATE')
and (orden <> 'WRITECHAR')
and (orden <> '') then
begin
writeln('Error: orden no reconocida. Se esperaba: CLS, LOCATE o WRITECHAR');
(*halt;*)
end;
end;


(* Genera el codigo de destino: codigo maquina de Amstrad CPC
dentro de un cargador en Basic *)
procedure generarCodigoUnaOrden;
var
x, y: byte;
letra: char;
codError: integer;
begin
if orden = 'CLS' then
begin
writeln( ficheroDestino, lineaActual,' DATA CD,6C,BB: '' CALL &BB6C - CLS' );
lineaActual := lineaActual + 10;
longitudTotal := longitudTotal + 3;
end;
if orden = 'WRITECHAR' then
begin
leerSimbolo('(');
leerSimbolo('''');
letra := obtenerLetra;
leerSimbolo('''');
leerSimbolo(')');
writeln( ficheroDestino, lineaActual,' DATA 3E,',
hexStr(ord(letra),2), ': '' LD A, "',letra, '"' );
lineaActual := lineaActual + 10;
writeln( ficheroDestino, lineaActual,' DATA CD,5A,BB: '' CALL &BB5A - WRITECHAR' );
lineaActual := lineaActual + 10;
longitudTotal := longitudTotal + 5;
end;
if orden = 'LOCATE' then
begin
leerSimbolo('(');
val(obtenerEntero,x,codError);
leerSimbolo(',');
val(obtenerEntero,y,codError);
leerSimbolo(')');
writeln( ficheroDestino, lineaActual,' DATA 3E,',
hexStr(x,2), ': '' LD A, ',x );
lineaActual := lineaActual + 10;
writeln( ficheroDestino, lineaActual,' DATA CD,6F,BB: '' CALL &BB6F - CURSOR COLUMN' );
lineaActual := lineaActual + 10;
writeln( ficheroDestino, lineaActual,' DATA 3E,',
hexStr(y,2), ': '' LD A, ',y );
lineaActual := lineaActual + 10;
writeln( ficheroDestino, lineaActual,' DATA CD,72,BB: '' CALL &BB72 - CURSOR ROW' );
(* 10 bytes *)
lineaActual := lineaActual + 10;
longitudTotal := longitudTotal + 10;
end;
end;

(* Genera el codigo de destino final: cargador Basic *)
procedure generarCodigoFinal;
begin
cerrarFicheroEntrada;

append( ficheroDestino );
writeln( ficheroDestino, lineaActual,' DATA C9: '' RET' );
writeln( ficheroDestino, lineaActual+10,' longitud = ',longitudTotal );
writeln( ficheroDestino, lineaActual+20,' MEMORY 39999' );
writeln( ficheroDestino, lineaActual+30,' FOR n=40000 TO 40000+longitud' );
writeln( ficheroDestino, lineaActual+40,' READ a$:POKE n,VAL("&"+a$)' );
writeln( ficheroDestino, lineaActual+50,' NEXT' );
writeln( ficheroDestino, lineaActual+60,' CALL 40000' );
close(ficheroDestino);
end;

23 marzo 2008

Un compilador sencillo paso a paso (2)

Este segundo acercamiento va a reconocer dos órdenes más: WRITECHAR, que escribirá una única letra en pantalla, y LOCATE, que moverá el cursor a otras coordenadas de pantalla. Además, en vez de aceptar una única orden por teclado, leerá varias desde disco.



Así, aceptará programas como éste:



cls
locate(10,5)
writeChar('a')
locate(1,10)
writeChar('b')



El hecho de que el usuario indique el nombre del fichero no es mucha complicación: vemos con "paramcount" si realmente lo ha detallado, avisamos si no lo ha hecho, y leemos el nombre con "paramstr(1)" en caso contrario:



if paramcount >= 1 then
nombreOrigen := paramstr(1)
else
begin
writeln('No se ha indicado el nombre del fichero');
halt;
end;
assign( ficheroOrigen, nombreOrigen );
reset( ficheroOrigen );



En cuanto a la traducción de writeChar('a'), se convertiría en dos órdenes de ensamblador: LD A, &61; CALL &BB5A. El primer problema es que tenemos que crear un código que no será siempre el mismo, sino que dependerá del parámetro que indiquemos entre comillas. Nuestra aproximación por ahora será "poco práctica":tomar la letra que aparece en la posición 12, y usar su código ASCII en hexadecimal:




letra := orden[12];
writeln( ficheroDestino, lineaActual,' DATA 3E,',
hexStr(ord(letra),2), ': '' LD A, "',letra, '"' );
writeln( ficheroDestino, lineaActual,
' DATA CD,5A,BB: '' CALL &BB5A - WRITECHAR' );


Además, para cada nueva línea que generemos, aumentaremos el número de línea del cargador en Basic y la cantidad de bytes de datos que hemos exportado:



lineaActual := lineaActual + 10;
longitudTotal := longitudTotal + 5;


Claramente, esta forma de saber la letra no es buena. ¿Qué ocurre si hay un espacio antes de las comillas? La letra que debemos escribir ya no estaría en la posición 12. Lo mismo ocurrirá si hay algún espacio antes de WRITECHAR o del paréntesis. Por eso, en la próxima aproximación crearemos un primer analizador léxico, que sea capaz de extraer correctamente una palabra, aunque tenga espacios delante.



Por lo que respecta a la traducción de LOCATE, tenemos otro problema: la posición puede ser un número de una cifra o de dos. Hasta que creemos un analizador léxico que nos ayude, de momento volveremos a hacerlo de forma artesanal: leeremos una primera cifra, miraremos si la siguiente es la coma o una cifra numérica, en caso de que sea numérica deberemos multiplicar por 10 la cifra anterior y sumársela... muy trabajoso, pero es lo que haremos por ahora; pronto lo mejoraremos.



En un CPC hay dos formas de situar el cursor: LD A, columna; CALL &BB6F y luego LD A, fila; CALL &BB72, o bien indicar a la vez la fila y la columna el registro HL (H=columna, L=fila) y hacer CALL &BB75. De momento usaremos la primera, que será algo más legible, aunque ocupe un poco más de espacio.



Con estas consideraciones, la parte que genera el código para la primera parte de LOCATE, la columna, todavía de forma muy rudimentaria y trabajosa, será:




posicion := 8;
x := ord(orden[posicion])-ord('0');
posicion := posicion + 1;
if orden[posicion] <> ',' then
begin
x := x*10 + ord(orden[posicion])-ord('0');
posicion := posicion + 1;
end;
writeln( ficheroDestino, lineaActual,' DATA 3E,',
hexStr(x,2), ': '' LD A, ',x );
lineaActual := lineaActual + 10;
writeln( ficheroDestino, lineaActual,' DATA CD,6F,BB: '' CALL &BB6F - CURSOR COLUMN' );
lineaActual := lineaActual + 10;



Así, el código generado para el programita anterior sería:




10 DATA CD,6C,BB: ' CALL &BB6C - CLS
20 DATA 3E,0A: ' LD A, 10
30 DATA CD,6F,BB: ' CALL &BB6F - CURSOR COLUMN
40 DATA 3E,05: ' LD A, 5
50 DATA CD,72,BB: ' CALL &BB72 - CURSOR ROW
60 DATA 3E,61: ' LD A, "a"
70 DATA CD,5A,BB: ' CALL &BB5A - WRITECHAR
80 DATA 3E,01: ' LD A, 1
90 DATA CD,6F,BB: ' CALL &BB6F - CURSOR COLUMN
100 DATA 3E,0A: ' LD A, 10
110 DATA CD,72,BB: ' CALL &BB72 - CURSOR ROW
120 DATA 3E,62: ' LD A, "b"
130 DATA CD,5A,BB: ' CALL &BB5A - WRITECHAR
140 DATA C9: ' RET
150 longitud = 33
160 MEMORY 39999
170 FOR n=40000 TO 40000+longitud
180 READ a$:POKE n,VAL("&"+a$)
190 NEXT
200 CALL 40000





Y el conjunto de todo el fuente del compilador quedaría así:




program cpcPaChi;

(* Un compilador de Pascal chiquitito para CPC
Por Nacho Cabanes - Version 0.02

Versiones hasta la fecha:

Num. Fecha Cambios
---------------------------------------------------
0.02 22-Mar-2008 Admite CLS, LOCATE, WRITECHAR,
que lee desde fichero. Solo permite
una orden en cada linea, con formato fijo

0.01 21-Mar-2008 Preliminar: solo acepta CLS por teclado
y genera el codigo correspondiente

*)



const
lineaActual: integer = 10;
longitudTotal: integer = 0;

var
ficheroOrigen, ficheroDestino: text;
nombreOrigen, nombreDestino: string;
orden: string;

(* Analiza las opciones que teclee el usuario. Por ahora,
solo el nombre del fichero a compilar *)

procedure analizarOpciones;
begin
if paramcount >= 1 then
nombreOrigen := paramstr(1)
else
begin
writeln('No se ha indicado el nombre del fichero');
halt;
end;
assign( ficheroOrigen, nombreOrigen );
reset( ficheroOrigen );
nombreDestino := 'salida.bas';
assign( ficheroDestino, nombreDestino );
rewrite( ficheroDestino );
end;

(* Obtiene una orden del programa fuente. La toma del fichero de origen *)
procedure obtenerOrden;
begin
readln(ficheroOrigen,orden);
end;


(* Analiza la orden que el usuario ha dado, y sale con un mensaje
de error si es incorrecta *)

procedure analizarUnaOrden;
begin
if (upcase(orden) <> 'CLS')
and (pos('LOCATE',upcase(orden))<>1)
and (pos('WRITECHAR',upcase(orden))<>1)
and (orden<>'') then
begin
writeln('Error: orden no reconocida. Se esperaba: CLS, LOCATE o WRITECHAR');
halt;
end;
end;


(* Genera el codigo de destino: codigo maquina de Amstrad CPC
dentro de un cargador en Basic *)

procedure generarCodigoUnaOrden;
var
x, y: byte;
posicion: byte;
letra: char;
begin
if upcase(orden) = 'CLS' then
begin
writeln( ficheroDestino, lineaActual,' DATA CD,6C,BB: '' CALL &BB6C - CLS' );
lineaActual := lineaActual + 10;
longitudTotal := longitudTotal + 3;
end;
if pos('WRITECHAR',upcase(orden))=1 then
begin
letra := orden[12];
writeln( ficheroDestino, lineaActual,' DATA 3E,',
hexStr(ord(letra),2), ': '' LD A, "',letra, '"' );
lineaActual := lineaActual + 10;
writeln( ficheroDestino, lineaActual,' DATA CD,5A,BB: '' CALL &BB5A - WRITECHAR' );
lineaActual := lineaActual + 10;
longitudTotal := longitudTotal + 5;
end;
if pos('LOCATE',upcase(orden))=1 then
begin
(* Leo X: 1 o 2 letras *)
posicion := 8;
x := ord(orden[posicion])-ord('0');
posicion := posicion + 1;
if orden[posicion] <> ',' then
begin
x := x*10 + ord(orden[posicion])-ord('0');
posicion := posicion + 1;
end;
writeln( ficheroDestino, lineaActual,' DATA 3E,',
hexStr(x,2), ': '' LD A, ',x );
lineaActual := lineaActual + 10;
writeln( ficheroDestino, lineaActual,' DATA CD,6F,BB: '' CALL &BB6F - CURSOR COLUMN' );
lineaActual := lineaActual + 10;
(* Tras la coma esta Y: 1 o 2 letras *)
posicion := posicion + 1;
y := ord(orden[posicion])-ord('0');
posicion := posicion + 1;
if orden[posicion] <> ')' then
begin
y := y*10 + ord(orden[posicion])-ord('0');
posicion := posicion + 1;
end;
writeln( ficheroDestino, lineaActual,' DATA 3E,',
hexStr(y,2), ': '' LD A, ',y );
lineaActual := lineaActual + 10;
writeln( ficheroDestino, lineaActual,' DATA CD,72,BB: '' CALL &BB72 - CURSOR ROW' );
(* 10 bytes *)
lineaActual := lineaActual + 10;
longitudTotal := longitudTotal + 10;
end;
end;

(* Genera el codigo de destino final: cargador Basic *)
procedure generarCodigoFinal;
begin
close(ficheroOrigen);

append( ficheroDestino );
writeln( ficheroDestino, lineaActual,' DATA C9: '' RET' );
writeln( ficheroDestino, lineaActual+10,' longitud = ',longitudTotal );
writeln( ficheroDestino, lineaActual+20,' MEMORY 39999' );
writeln( ficheroDestino, lineaActual+30,' FOR n=40000 TO 40000+longitud' );
writeln( ficheroDestino, lineaActual+40,' READ a$:POKE n,VAL("&"+a$)' );
writeln( ficheroDestino, lineaActual+50,' NEXT' );
writeln( ficheroDestino, lineaActual+60,' CALL 40000' );
close(ficheroDestino);
end;

(* Cuerpo del programa *)
begin
analizarOpciones;
while not eof(ficheroOrigen) do
begin
obtenerOrden;
analizarUnaOrden;
generarCodigoUnaOrden;
end;
generarCodigoFinal;
end.

21 marzo 2008

Un compilador sencillo paso a paso (1)

Voy a hacer un pequeño proyecto para estas vacaciones: un compilador sencillo.

No soy ningún experto en creación de compiladores, y por eso será un compilador sencillo y lo haré paso a paso. Quizá estos pasos ayuden a alguien más a entender un poco cómo funcionan estas herramientas, así que los dejaré disponibles aquí.

Más detalles:
  • Quiero crear el compilador en un lenguaje "razonablemente fácil de leer" (y por tanto de depurar) y que esté disponible para muchas plataformas. Por eso usaré el lenguaje Pascal como lenguaje "anfitrión". En concreto, yo desarrollaré desde Free Pascal para Windows.
  • Se tratará también de un compilador de lenguaje Pascal (realmente, de un lenguaje "parecido a Pascal"). Nuevamente por facilidad de lectura y de depuración, pero eso además da la posibilidad de que quizá algún día el compilador se pueda "compilar a sí mismo" y se puedan crear programas directamente desde la "máquina de destino".
  • En cuanto a esa máquina de destino, tiene que ser un sistema "razonablemente sencillo", pero considero más motivador usar un sistema real que un sistema "inventado". Por eso, usaré una arquitectura que a la vez es sencilla y fácil de programar: un ordenador "clásico" de los años 80, el Amstrad CPC, que usa un Z80 de 8 bits como procesador y que permite hacer muchas tareas de "alto nivel" mediante simples llamadas al firmware.
La mayoría de libros de compiladores comienzan por crear un analizador léxico, luego uno sintáctico, y van ampliando sucesivamente hasta llegar a crear un compilador completo. Mi aproximación va a ser radicalmente diferente. Con la esperanza de obtener algo "totalmente funcional" (aunque muy limitado) desde un primer momento, yo haré lo siguiente:
  • Crear un primer compilador que sólo reconozca una orden muy simple, avise en caso de error y genere el código de destino correspondiente.
  • Ampliar para que reconozca tres órdenes distintas, con parámetros sencillos, y las acepte en cualquier orden.
  • Ampliar nuevamente para que fuerce a una estructura de programa concreta (el programa deberá comenzar por "program", el cuerpo deberá estar entre "begin" y "end", las órdenes deberán terminar en "punto y coma").
  • Añadir la posibilidad de declarar y usar variables.
  • Poder hacer comparaciones simples ("if").
  • Añadir variables de más de un tipo (numéricas, carácter) y hacer las comprobaciones de tipo correspondientes.
  • Permitir la creación de procedimientos ("procedure"), que permitan crear programas modulares para la máquina objetivo.
  • ...
Este primer acercamiento reconocerá sólo la orden CLS, encargada de borrar la pantalla. La orden en lenguaje ensamblador de un Amstrad CPC equivalente es CALL &BB6C, que en código máquina sería la secuencia de bytes CD, 6C, BB (expresados en hexadecimal).

Para simplificar la prueba de los programas destino creados, el compilador no generará ensamblador. Ni siquiera creará un fichero ".BIN" de código máquina, sino un fuente en Basic capaz de "generar ese código máquina". Así, para una única orden CLS, se obtendría el siguiente programa, que lee los bytes en hexadecimal, los coloca a partir de la dirección 40.000 de la memoria y finalmente llama a esa dirección para poner el programa en marcha:


10 DATA CD,6C,BB: ' CALL &BB6C
20 DATA C9: ' RET
30 longitud = 3
40 MEMORY 39999
50 FOR n=40000 TO 40000+longitud
60 READ a$:POKE n,VAL("&"+a$)
70 NEXT
80 CALL 40000


Con todas estas consideraciones, el primer acercamiento al compilador es muy sencillo:
  • Un procedimiento que obtiene una orden (sólo una) del usuario, desde el teclado.
  • Un procedimiento que analiza la orden, y da un mensaje de error si no es CLS.
  • Un procedimiento que genera el código de destino (que por ahora es fijo).

El fuente sería así:




program cpcPaChi;

(* Un compilador de Pascal chiquitito para CPC
Por Nacho Cabanes - Version 0.01, preliminar

Versiones hasta la fecha:

Num. Fecha Cambios
---------------------------------------------------
0.01 21-Mar-2008 Preliminar: solo acepta CLS por teclado
y genera el codigo correspondiente
*)


var
ficheroDestino: text;
orden: string;


(* Obtiene una orden del programa fuente. En esta version, solo
es una unica orden, y solo se admite "CLS" *)
procedure obtenerOrden;
begin
write('Introduzca la orden a traducir: ');
readln(orden);
end;

(* Analiza la orden que el usuario ha dado, y sale con un mensaje
de error si es incorrecta *)
procedure analizarOrden;
begin
if upcase(orden) <> 'CLS' then
begin
writeln('Error: orden no reconocida. Se esperaba: CLS');
halt;
end;
end;

(* Genera el codigo de destino: codigo maquina de Amstrad CPC
dentro de un cargador en Basic *)
procedure generarCodigo;
begin
assign( ficheroDestino, 'salida.bas' );
rewrite( ficheroDestino );
writeln( ficheroDestino, '10 DATA CD,6C,BB: '' CALL &BB6C' );
writeln( ficheroDestino, '20 DATA C9: '' RET' );
writeln( ficheroDestino, '30 longitud = 3' );
writeln( ficheroDestino, '40 MEMORY 39999' );
writeln( ficheroDestino, '50 FOR n=40000 TO 40000+longitud' );
writeln( ficheroDestino, '60 READ a$:POKE n,VAL("&"+a$)' );
writeln( ficheroDestino, '70 NEXT' );
writeln( ficheroDestino, '80 CALL 40000' );
close(ficheroDestino);
end;

(* Cuerpo del programa *)
begin
obtenerOrden;
analizarOrden;
generarCodigo;
end.

06 marzo 2008

Para nostálgicos: RetroMadrid

Quedan menos de dos días...

Si viviste lo que ahora ya se conoce como "informática clásica" (el auge de los primeros ordenadores domésticos, a mediados de los años 80), ésta es una de esas citas que debes conocer.

Podrás ver un museo de 8/16 bits, rarezas, conocer a alguno de los aguerridos programadores que todavía hoy en día crean software para ese tipo de ordenadores, participar en alguno de los concursos... Si vives cerca de Madrid, deberías acercarte.

Para más información, su sitio web es www.retromadrid.es

Crear ficheros de ayuda de Windows (.CHM)

(Respondiendo a una consulta que he recibido hace poco por e-mail)

Crear ficheros de ayuda de Windows es sencillo. Las pautas básicas a seguir para crear un fichero elemental son:

  • Crear la ayuda, en formato HTML (una página web o varias enlazadas entre ellas).
  • Crear un pequeño fichero de texto, que será la descripción del fichero de ayuda (más adelante incluyo un ejemplo).
  • Descargar el "HTML Help SDK", una pequeña utilidad gratuita de Microsoft, de unos 3.3 Mb de tamaño, e instalarla.
  • Entrar al entorno (el fichero llamado HHW), abrir desde allí el fichero de descripción que habíamos creado y pulsar el botón "Compile HTML file".

Eso es todo (si no queremos índices ni otras características avanzadas, claro).

Un ejemplo de fichero, que crearíamos con cualquier editor de texto (incluso el "Bloc de notas" serviría) y que guardaríamos con el nombre "ejemplo.hhp" (el nombre puede ser cualquiera, pero la extensión HHP es importante).


[OPTIONS]
Compatibility=1.1 or later
Compiled file=ejemplo.chm
Default topic=index.htm
Display compile progress=No
Language=0xc0a Español (España)
Title=Ejemplo de fichero de ayuda

[FILES]
index.htm


Creo que todas las opciones son autoexplicativas. No hace falta indicar todos los ficheros en el apartado FILES. Basta con indicar cual es el principal, y automáticamente se incluirán todos los ficheros HTML y las imágenes que estén enlazados a partir de él.

En muy pocos segundos tendremos un único fichero comprimido que agrupa toda nuestra estructura web, incluso imágenes.