- Usar FreePascal, que es muy compatible con Turbo Pascal 7, pero no da problemas con Windows 7 64 bits. De hecho, hay versiones incluso para Windows, Linux y Mac OS. Además, se puede elegir entre el editor en modo texto (al estilo del Turbo Pascal original) o un entorno más moderno "al estilo Windows", si se descarga todo el paquete llamado Lazarus.
- Usar el propio Turbo Pascal (o Turbo C, o el programa del que se trate) desde algún "emulador de MsDos. El más senciillo de usar es DosBox, pero tiene un problema: no viene "totalmente instalado y listo para usar", porque no crea un "disco C" en el que instalar nuestro compilador. Una alternativa más cómoda es usar "D-Fend Reloaded", que es un entorno mejorado que incluye DosBox, pero lo instala junto con varias utilidades, y deja un "disco C" listo para usar. Además, es fácil copiar cosas desde Windows en ese "falso disco C", porque es una carpeta llamada "VirtualHD" que se encuentra en nuestra carpeta de usuario: C:\Documents and Settings\Usuario\D-Fend Reloaded\VirtualHD Todo lo que copiemos en esa carpeta (por ejemplo, los ficheros de instalación de nuestro compilador) estará visible cuando entremos a D-Fend Reloaded. Una última consideración: en Turbo Pascal, Borland C++ y toda esa familia de productos se pulsaba Ctrl+F5 para lanzar nuestro programa, pero esa tecla está reservada en DosBox para hacer una captura de pantalla, por lo que deberemos ejecutar nuestros programas entrando al menú "Run".
06 junio 2011
Turbo Pascal en Windows 7 64 bits
30 abril 2011
Bad image format exception en 64 bits (juegos con SDL que dejan de funcionar)


07 septiembre 2008
Un compilador sencillo paso a paso (17 - Alguna función predefinida)
Las expresiones matemáticas las trataremos en el siguiente acercamiento. Ahora vamos a añadir alguna función predefinida, como el anterior "readkey", que nos permita ver qué tecla ha pulsado el usuario.
Antes hacíamos asignaciones a variables a partir de valores que eran constantes numéricas o constantes con nombre, de modo que se podía solucionar apenas en dos pasos:
LD A, valor
LD (variable), A
Ahora el primer paso cambiará ligeramente: guardará el valor en el registro A, pero este valor puede provenir de diversos sitios: será "LD A, (variable)" si viene de una variable, o pasaremos el testigo a la función si corresponde. Muchas de las funciones predefinidas para el CPC devuelven su resultado en el registro A, como CALL &BB18, que espera a que se pulse una tecla; otras devuelven el resultado en otros registros, como HL; y tendremos que extraer lo que nos interese con órdenes como "LD A, H"; otras activan flags como C o Z, y entonces tendremos que usar estructuras de control como RET Z: nos podría interesar emplear secuencias como "LD A, 1 - RET NZ - LD A, 0 - RET" (cargar 1 en A y volver si el flag Z no está activo; en caso contrario, cargar 0 en A y volver).
Vamos a aprovechar para hacer otra mejora: los programas que genera nuestro compilador tienen un tamaño relativamente grande, si tenemos en cuenta que la arquitectura de destino es un ordenador que deja libre poco más 40 Kb para programas de usuario. Parte de ese tamaño (cerca de la mitad) se debe a la cantidad de comentarios que se incluyen en el código generado. Estos comentarios son útiles a la hora de depurar fallos de generación de código, pero deberían ser innecesarios (o casi) cuando el compilador esté terminado. Por eso, es razonable añadir la posibilidad de eliminar los comentarios en el código de destino. Para eso, cambiaremos la forma de generar código. Antes se usaban cosas "muy artesanales" como:
writeln( ficheroDestino, lineaActual,' DATA CD,5A,BB: '' CALL &BB5A - WRITECHAR' );
lineaActual := lineaActual + 10;
longitudTotal := longitudTotal + 3;
El código alternativo actual podría ser una función "genCodigo" (generar codigo), que permita crear una orden DATA con una secuencia de hasta 3 bytes, que es lo más habitual en nuestra arquitectura de destino. Por si alguna orden es más corta, el primer parámetro será la longitud real (si es menor que tres, se despreciaría el valor de alguno de los parámetros). El último parámetro será el comentario, de forma que se pueda omitir con facilidad:
genCodigo(3, 'CD', '5A', 'BB', 'CALL &BB5A - WRITECHAR' );
De paso, vamos a añadir alguna posibilidad mas, como que WriteChar permita usar variables, no sólo textos prefijados, aprovechando la misma rutina de lectura de valores que hemos utilizado en las asignaciones. Ahora serán válidas también expresiones como
writeChar(letra);
(aunque como sólo tenemos un tipo de variables, byte, esa variable "letra" debería ser de tipo "byte", todavía no de tipo "char", que es lo que sería más razonable).
Con estos cambios ya se podrán hacer secuencias de órdenes como:
writeString('Pulsa una tecla: ');
tecla := readkey;
writeString('Has pulsado: ');
writeChar(tecla);
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/
01 septiembre 2008
Un compilador sencillo paso a paso (16 - Validando y ampliando)
La forma habitual es crear una serie de casos de prueba cuyo resultado sea conocido. Para nosotros se trataría de fuentes de prueba que permitan comprobar que se reconoce correctamente el lenguaje de origen y que se convierte correctamente al lenguaje de destino.
Deberíamos poner bajo prueba cada una de las posibilidades del lenguajes. Nosotros lo haremos de forma un poco menos exhaustiva (y, por tanto, menos fiable), con un único fuente que utilice todo lo que hemos implementado: constantes, variables, saltos, escritura de caracteres y de cadenas, cambio de modo de pantalla y de colores, comprobación de condiciones ciertas y falsas, repeticiones de varios tipos...
El fuente de prueba ha sido éste:
program ej16; { Ejemplo 16 de CpcPaChi }
(* Prueba todas las funcionalidades de
CpcPaChi disponibles hasta la version
0.16 *)
const
MODO = 1;
var
i, j, k: byte;
label
escribirSinCambiarColor;
procedure inicializa;
begin
cpcMode(MODO);
cpcInk(0,BLACK);
cpcInk(1,BRIGHTCYAN);
cpcInk(2,BRIGHTBLUE);
cpcInk(3,BLUE,CYAN);
{ paper(0);}
pen(2);
end;
begin
inicializa;
locate(2,2);
writeString('Prueba de FOR: 3 asteriscos ');
for i := 1 to 3 do
begin
pen(i);
writeChar('*');
end;
pen(1);
locate(2,3);
writeString('Prueba de IF: 3 es ');
i := 3;
if i=3 then writeString('=3 ');
if i>3 then writeString('>3 ');
if i>2 then writeString('>2 ');
if i>=3 then writeString('>=3 ');
if i>=4 then writeString('>=4 ');
if i<3 then writeString('<3 ');
if i<4 then writeString('<4 ');
if i<=3 then writeString('<=3 ');
if i<=2 then writeString('<=2 ');
if i<>2 then writeString('<>2 ');
if i<>3 then writeString('<>3 ');
pen(2);
locate(2,4);
writeString('Prueba de WHILE: 6+ ');
j := 5;
while j <= 10 do
begin
writeChar('+');
inc(j);
end;
pen(2);
locate(2,5);
writeString('Prueba de REPEAT: 5- ');
pen(3);
k := 10;
repeat
writeChar('-');
dec(k);
until k = 5;
locate(2,6);
pen(1);
writeString('For sin begin..end: ');
for j := 1 to 10 do
begin
writeChar('.');
end;
locate(2,7);
writeString('Prueba de GOTO');
goto escribirSinCambiarColor;
pen(2);
escribirSinCambiarColor:
writeString(' (Sin cambiar color)');
locate(2,8);
for i := 5 to 3 do
writeString('Esto no deberia escribirse');
end.
Con él, se ha podido descubrir varios fallos que tenía la versión 0.15, como:
- Al añadir la comprobación basada en Tokens, había introducido un fallo en la declaración de constantes: debía terminar en coma, en vez de en punto y coma.
- Todavía no estaba creado el código para reconocer correctamente las comparaciones , >=.
- Cuando se insertaban elementos en la tabla de símbolos, no se comprobaba si ésta ya se había llenado.
- En la segunda pasada, sólo se buscaba una coincidencia parcial de una cadena dentro de otra, lo que podría hacer que la etiqueta FIN_IF_10 se reemplazara por el valor de FIN_IF_1.
También ha servido para alguna pequeña mejora adicional como:
- En el analizador sintáctico, la variable "orden" era global, lo que podría llegar a dar problemas cuando una función de análisis llame a otra distinta, como por ejemplo el caso de un IF o un FOR que contengan una sentencia compuesta.
- Teníamos un "devolverLetra" que nos venía bien en el análisis sintáctico, para poder leer una letra más adelante (y así distinguir por ejemplo entre un : y un := ) pero no teníamos un "devolverToken", que es cómodo para situaciones como la comprobación de un ELSE.
- La compatibilidad con Turbo Pascal 7 era limitada, por algún detalle como usar la función "upcase" para convertir toda una frase a mayúsculas, mientras que TP7 sólo permite usarla para un "char". Como primer acercamiento a una compatibilidad mejorada, he creado una función "mayusc" que convierte un string, usando "upcase" para cada letra.
- El analizador léxico tenía una variable "depurando" que permitía ver más información de la habitual en pantalla; ahora esta posibilidad se encuentra también en el analizador sintático.
De paso, se podía haber hecho alguna otra mejora pendiente, como que el espacio para variables se reserve al final y no al principio, o que las cadenas de texto se almacenen al final del código, en vez de entre medias, para que sea más legible el código máquina resultante. Pero he preferido no hacerlo, para que todas las versiones tengan una cantidad de trabajo similar: una cantidad de trabajo mayor en un tiempo parecido puede suponer una gran cantidad de fallos... y no hay necesidad...
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/
28 agosto 2008
Un compilador sencillo paso a paso (15 - Pequeñas mejoras)
Para solucionarlo, tenemos dos alternativas. La rápida pero mala es permitir que los "símbolos" estén formados por más de un carácter. La correcta es empezar a hablar (por fin) de "tokens". Así, tanto "if" como "+" o "<=" o la variable "x" serían tokens, cada uno de ellos con un "valor" o "lexema" y también con un "tipo" (junto con otros datos que nos pueden ser útiles, como la fila y columna en que se encuentra). Esta estructura estaba preparada desde la versión 0.07, pero no la habíamos utilizado hasta ahora.
Además aprovecharemos para corregir las asignaciones: los símbolos ":=" podían estar separados, por lo que hasta ahora ": =" se habría interpretado como una asignación válida. Nos vendrá bien usar, además de la función "leerCaracter" otra "devolverCaracter" que será necesaria cuando leamos de más al comprobar si al ":" le sigue un "=".
Además, orientado a la máquina de destino añadiremos una orden cpcInk, que permita jugar con la paleta de colores, redefinir color haciendo cpcInk(1,WHITE); o bien cpcInk(1,BLUE, RED); (este último formato parpadearía entre dos colores). Estas constantes para los colores estarán definidas también en el compilador, para que el código sea más legible. También añadiremos una orden WriteString, que escriba una cadena (prefijada) en vez de sólo una letra.
Ahora podremos hacer cosas como:
program ej15; { Ejemplo 15 de CpcPaChi }
var
i, j, k: byte;
procedure inicializa;
begin
cpcMode (1);
cpcInk(0,BLACK);
cpcInk(1,WHITE);
cpcInk(2,BRIGHTYELLOW);
cpcInk(3,PALEYELLOW);
paper(0);
end;
procedure lineaHorizontal;
begin
for i := 1 to 10 do
writeChar('*') ;
end;
(* Vamos a dibujar un cuadrado *)
begin
inicializa;
locate(2,2);
writeString('Alla vamos...');
locate(10,3);
pen(1);
lineaHorizontal;
pen(2);
for j := 4 to 7 do
begin
locate(10,j);
writeChar('*');
for k := 1 to 8 do
writeChar(' ');
writeChar('*');
end;
locate(10,8);
pen(3);
lineaHorizontal;
locate(20,20);
writeString('Hemos terminado!');
pen(1);
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/
23 agosto 2008
Un compilador sencillo paso a paso (14 - Acercamiento a los procedimientos)
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;
20 agosto 2008
Un compilador sencillo paso a paso (13 - Estructuras repetitivas)
Comencemos por "while". Un bloque como
while condicion do
begin
bloque1
end
restoDePrograma
Se podría reescribir como
inicioWhile:
if not condicion then goto inicioRestoDePrograma;
bloque1
goto inicioWhile;
inicioRestoDePrograma:
restoDePrograma
Que, al igual que ocurría con "if", ya tiene una traducción casi directa a ensamblador (y, por tanto, a código máquina).
El caso de "repeat..until" es aún más sencillo. El código fuente sería
repeat
bloque1
until condicion;
restoDePrograma
Se podría reescribir en forma muy cercana al ensamblador como
inicioRepeat:
bloque1
if not condicion then goto inicioRepeat;
restoDePrograma
Mientras que el bucle "for" es apenas ligeramente más complicado:
for variable := valorInicial to valorFinal do
begin
bloque1
end
restoDePrograma
Quedaría como
variable := valorInicial;
inicioFor:
if variable > valorFinal then goto inicioRestoDePrograma;
bloque1
inc(variable)
goto inicioFor;
inicioRestoDePrograma:
restoDePrograma
(ignoramos por ahora el caso de usar "downto" en vez de "to").
Aun así, hay cosas que mejorar (en una versión posterior). Unas son fallos claros de la versión actual y otras son mejoras necesarias. La primera y más evidente es que nuestro analizador léxico no reconoce operadores de formados por dos letras: ni "<=", ni ">=", ni "<>". Tampoco se crea correctamente el código objetivo para estas comparaciones. Por supuesto, tampoco permite AND, OR y NOT para formar condiciones más complejas. Tampoco sabemos analizar expresiones matemáticas más elaboradas, de modo que no podemos hacer cosas como "if a > x+1 then ..."
Todo irá llegando... ;-)
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/
18 agosto 2008
Un compilador sencillo paso a paso (12 - Condiciones simples)
La idea es que un bloque como
if condicion then
begin
bloque1
end
else
begin
bloque2
end
restoDePrograma
Se podría reescribir como
if not condicion then goto inicioBloque2;
begin
bloque1
goto inicioRestoDePrograma;
end;
inicioBloque2
begin
bloque2
end
inicioRestoDePrograma:
restoDePrograma
Y esa construcción ya tiene una traducción casi directa a ensamblador (y, por tanto, a código máquina).
En cuanto a las condiciones, nos centraremos por ahora en condiciones muy sencillas, del estilo de "if x > 2 then ...".
La forma en que lo haremos en código máquina de Z80 será la siguiente:
- El primer valor a comparar lo guardaremos en el registro A.
- El segundo valor lo guardaremos en el registro B.
- Compararemos los valores guardados en ambos registros, usando la orden CP (que realmente hace una resta entre ambos valores).
- Usaremos el "flag" adecuado para cada tipo de comparación: para "if a = b" usaríamos "JP Z" (saltar si se activa el flag Z porque el resultado de la última operación sea cero); para "if a <> b" usaríamos "JP P" (saltar si resultado positivo).
También es necesario algún otro cambio pequeño en el fuente, para permitir bloques begin...end, pero eso es muy sencillo...
11 agosto 2008
Un compilador sencillo paso a paso (11 - Saltos incondicionales)
Eso supone que también podamos definir etiquetas, para indicar a qué lugar queremos saltar. En Pascal, quedaría algo como:
label destino;
begin
...
destino:
...
goto destino;
...
end.
La forma más sencilla (aunque poco eficiente) de convertirlo a código máquina es usar saltos a direcciones absolutas de memoria. Por ejemplo, un salto atrás en ensamblador quedaría así:
...
.destino
...
jp destino
...
La traducción a ensamblador es prácticamente inmediata. Pero nosotros no queremos compilar a ensamblador, sino directamente a código máquina, de modo que no podemos emplear etiquetas, sino que debemos indicar a qué posicion de memoria queremos saltar. Si se trata de una posición de memoria anterior, no es díficil: como ya la conocemos, podríamos construir sin problemas la orden de salto:
30000: ...
...
C3 30 75; jp 30000
...
Pero si se trata de un salto hacia un punto posterior, estamos atados de manos: todavía no sabemos de qué dirección de memoria se trata, lo que nos obliga a "retroceder" cuando ya la conozcamos: en ensamblador sería
...
jp destino
...
.destino
...
Que en código máquina se convertiría en:
...
C3 ?? ?? ; jp ????
...
Esas "interrogaciones" representan lo que todavía no sabemos, huecos que tendremos que rellenar más adelante, y que nos obligarán a dar una segunda pasada (aunque sencilla). Como estamos volcando a fichero a medida que compilamos, tenemos dos opciones
- Conservar todo el programa en memoria, para poder dar la segunda
pasada antes de volcar a disco. - Tras terminar la primera pasada, volver a abrir el fichero generado
para dar la segunda pasada, volcando el resultado a un nuevo fichero.
La primera opción sería la más eficiente en cuanto a velocidad, pero necesita más espacio en memoria, y partimos de la idea de que quizá algún día el compilador llegue a poder compilarse a sí mismo y funcionar desde la máquina de destino, un equipo con unos 64 Kb de memoria. Por eso, consideraremos la memoria como un bien muy valioso, y seguiremos volcando a disco, para dar una segunda pasada sobre ese fichero resultante en disco.
Así, el resultado de la primera salida para esa instrucción de
salto podría ser algo como:
...
DATA,C3,destinoLOW,destinoHIGH ; jp destino
...
(Lo de descomponer "destino" en "destinoLOW" y "destinoHIGH" es porque en la máquina que estamos tomando como destino se sigue el criterio habitual de codificar los datos de más de un byte como el byte menos significativo seguido por el más significativo -"little endian").
La segunda pasada podría buscar cada uno de los identificadores de la tabla de símbolos (como "destino") para ver si aparecen descompuestos en "destinoLOW" y "destinoHIGH" y reemplazarlos por sus valores.
Ya que vamos a dar dos pasadas, podemos optimizar algo que antes habíamos hecho de una forma sencilla pero no demasiado correcta:podemos dejar las variables a continuación del código, en vez de colocarlas al principio y separadas del código por un espacio arbitrario. Ahora otros fragmentos de nuestro código serán cosas como:
...
DATA,3E,[xBYTE] ; LD A, variableX
...
Y el espacio para esa "variableX" se reservaría al final, de la primera pasada, tras el código del programa, y el valor exacto de la dirección se sustituiría en la segunda pasada.
Aun así, esa mejora de momento la aplazamos para algo más adelante...
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/
06 agosto 2008
Un compilador sencillo paso a paso (10 - Variables utilizables)
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/
22 junio 2008
Un compilador sencillo paso a paso (9 - Variables, primer acercamiento)
Ya habíamos preparado una primera tabla de símbolos, que nos permitía utilizar constantes en los programas. Ahora vamos a ampliar ligeramente la estructura para permitir el manejo básico de variables.
De cada símbolo estábamos guardando el nombre, el tipo (por ahora sólo byte) y el valor. En un principio, nos podría bastar para un manejo básico de variables a nuesto nivel. Aun así, hay otra diferencia entre el manejo de constantes y variables: una constante se declara a la vez que se le da un valor, pero una variable no: primero se declara y luego se le da un valor. Esto puede provocar que intentemos usar una variable que se ha declarado pero que todavía no tiene un valor. Hay tres formas de atacar este problema:
- Ignorarlo, con lo cual se podrá acceder al valor de una variable a la que realmente no se ha dado valor, y que contendrá basura. Es lo que se suele hacer e los compiladores de C.
- Dar un valor a las variables cuando se declaren. Por ejemplo, se podría dar por sentado que una variable numérica tendrá un valor inicial de cero. Es lo que se suele hacer en muchas versiones de Basic.
- Anotar si se ha dado un valor a la variable o no, y mostrar un mensaje de error si se intenta usar una variable que no tiene valor. En el momento de compilar se puede hacer una primera estimación, y avisar de los posibles errores, como se hace en Java, pero la mayoría de comprobaciones "reales" habría que hacerlas en tiempo de ejecución.
Yo seguiré la primera aproximación: reservaré un espacio para cada variable, no le daré un valor incial, y no comprobaré si la variable tenía valor cuando se acceda a ella.
Habrá que modificar la tabla de símbolos ligeramente, porque de las variables no guardaremos su valor actual, sino la dirección de memoria en que se encuentran.
También habrá que ampliar ligeramente el analizador léxico: ahora, además del bloque CONST, podrá haber un bloque VAR, y ambos serán opcionales, por lo que esa parte del análisis sintáctico podría ser:
(* const o var, antes de begin *)
repeat
orden := upcase(obtenerIdentificador);
if orden = 'CONST' then
begin
analizarCONST;
end;
if orden = 'VAR' then
begin
analizarVAR;
end;
until orden = 'BEGIN';
El análisis sintáctico de la orden VAR sería muy parecido al de CONST:
procedure analizarVAR;
var
ident: string;
tipoVar: string;
terminador: char;
begin
ident := upcase(obtenerIdentificador);
{ Despues del identificador podra ir :tipo o ,var }
{ De momento, solo una variable }
leerSimbolo(':');
tipoVar := upcase(obtenerIdentificador);
if (tipoVar <> 'BYTE') then
errorFatal('Tipo incorrecto');
terminador := leerCualquierSimbolo;
if (terminador <> ';') then
errorFatal('Terminador incorrecto');
insertarSimboloVar(ident, 'b');
end;
Como se puede ver, ahí aparece una orden "errorFatal" que no existía antes. Es una pequeña mejora al análisis léxico: llevar cuenta de la línea y la columna en que estamos, para poder dar mensajes de error un poco más detallados. Reemplazará a los WRITELN con mensajes de error seguidos de HALT que estábamos usando hasta ahora :
(* Muestra un mensaje de error y abandona el programa *)
procedure errorFatal(mensaje: string);
begin
write('Linea: ',lineaActual,'. Pos: ',posicionLineaActual+1,'. ');
writeln(mensaje);
halt;
end;
La parte complicada del manejo de variables no es ésta, sino la que está por llegar. Habrá que dar valores a variables, que se guardarán en la posición de memoria que le hayamos reservado. Se deberá poder leer el valor de una variable. Se deberá poder hacer operaciones simples, como el incremento (INC) y el decremento (DEC). Eso lo haremos en el próximo acercamiento. Otras operaciones más complejas, como las aritméticas y las comparaciones tardarán un poco más en llegar...
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/
12 mayo 2008
Un nuevo sitio sobre Free Pascal en español
Su página oficial es www.freepascal.org, pero lleva un tiempo fallando con mucha frecuencia. Por eso, y para dar un servicio adicional a aquellos que no hablen inglés, nace freepascal.es. Desde allí hay acceso a descargas del compilador, así como de Lazarus (el clónico de Delphi desarrollado a partir de Free Pascal), enlaces a fuentes de ejemplo, a cursos y foros...
23 abril 2008
Un compilador sencillo paso a paso (8 - Mejorando el analizador léxico)
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)
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)
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/
31 marzo 2008
Un compilador sencillo paso a paso (5)
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)
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.
- 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.