> ÍNDICE DE REVISTAS <
Número 17 - Marzo 2009 ZX Certified webmaster speccy.org
 

ZX BASIC. Un sueño hecho real...

Este artículo trata sobre ZX BASIC, un compilador cruzado que permite crear en tu PC programas para tu ZX Spectrum. Lo que hace es traducir las instrucciones de un programa en BASIC a código máquina del Z80 que luego puedes ejecutar en un emulador o en un Spectrum real.

Hace mucho tiempo...

Ejem... ¿En una galaxia muy muy lejana? Bueno, no. No tanto.

Al igual que muchos de vosotros, de niño quería hacer otras cosas con mi ZX Spectrum aparte de jugar: quería experimentar. Programar era una forma de hacerlo, sin duda. La electrónica no era mi fuerte y siendo un crío como era y con tan poca experiencia (había tenido algún que otro susto con la electricidad anteriormente) la programación era la mejor forma de experimentar. Era como ser un dios en miniatura dentro del universo de posibilidades que el ZX Spectrum ofrecía por aquel entonces. Compraba MicroHobby (y las primeras Micromanía); me dedicaba -como creo que todos hicimos- horas y horas a teclear los listados publicados; a hacer mis propios códigos en BASIC; a cargar algunos de los programas y modificarlos (hoy diríamos hackearlos) y en definitiva a probar muchas otras cosas de esta pequeña pero maravillosa máquina.

Pronto una limitación se hizo evidente: el BASIC del ZX Spectrum era ciertamente lento. Cuando nos picábamos mis amigos y yo con nuestros respectivmos micros, el ZX Spectrum siempre llevaba las de perder. Un simple bucle FOR n = 1 TO 1000: NEXT n tardaba algo más de 4 segundos en ejecutarse aunque no hiciera nada. En otros microordenadores esto no era así (en el Dragon 64 tardaba menos de un segundo, creo recordar).

Pese a todo, el BASIC de Sinclair era extremadamente rico en general. Aprendí programación con él y muchas de esas cosas me ayudaron en la carrera de informática. Esa riqueza del lenguaje no parecían tenerla los otras implementaciones BASIC de los años 80 (o esa fue mi impresión). Un ejemplo típico es el COMMODORE, cuyo BASIC emplea muchos POKES para realizar determinadas tareas. Es por esto que creo que es uno de los micros con mayor cantidad de programas BASIC creados (todavía a día de hoy).

Para vencer la limitación de la velocidad, muchos intentamos programar en ensamblador. Mis escasos conocimientos en general y particularmente sobre todo de aritmética binaria y hexadecimal, la escasa bibliografía -gracias de nuevo a MicroHobby y a sus fichas- y, sobre todo, de un ensamblador decente (nunca tuve GENS ni MONS) hicieron que el sueño de crear un juego y otras muchas cosas nunca se cumplieran.

Llegué a hacer en BASIC un intérprete de lenguaje Prolog muy simple. Otro de LOGO y otro de un lenguaje que me inventé. Desde luego, no tenía el conocimiento sobre la programación de lenguajes de ordenador que tengo hoy día, pero como ya os imagináis, si el BASIC de ZX era lento, un intérprete de un lenguaje realizado en BASIC era aún más lento. Exasperante. A pesar de todo, esa pequeña frustración fue la semilla que fijó claramente mi vocación (como supongo que muchos de vosotros).

Si estás leyendo esto es porque muy probablemente también tuviste estas inquietudes. Ahora volvamos al presente...

Un poco de teoría

Antes de comenzar a explicar el uso del compilador, está bien echar un vistazo por encima a qué es un compilador y su diseño interno por dos motivos:

  • Hay personas que saben programar pero no comprenden que hay cosas que un compilador no podrá hacer jamás.
  • También puede que te interese expandir el compilador de alguna manera, o contribuir al desarrollo del lenguaje o a muchas otras cosas.

Para empezar, un traductor no es más que un programa capaz de convertir de alguna manera un código escrito en un lenguaje de programación a otro. Por ejemplo puedes tener un programa en BASIC y traducirlo a LOGO (suponiendo que sea factible) o a C. Un compilador va un paso más allá. Es un traductor que traduce un lenguaje fuente a código objeto (binario) que es directamente ejecutable por la máquina.

Un intérprete, por contra, lo que hace es ir leyendo un programa fuente instrucción por instrucción y representarlas ejecutando una serie de acciones equivalentes a cada una de ellas. Si usaste un ZX Spectrum ya conoces un intérprete: la ROM del ZX Spectrum. La ROM es en su mayor parte un intérprete de Sinclair BASIC. Los programas interpretados, por lo general, son más lentos que los compilados (en algunos pocos casos se puede conseguir una velocidad casi similar).

ZX BASIC es un compilador cruzado. Los compiladores cruzados son aquellos que se ejecutan en una máquina pero producen código objeto para otra. En este caso, vamos a usar ZX BASIC en nuestro PC (ya sea sobre Windows o Linux) para producir un programa compilado para ZX Spectrum (en realidad para Z80). De esa manera ahorraremos memoria -que en el Spectrum es muy escasa- al no tener que alojar el compilador junto con el programa compilado. Además, la velocidad de compilación será mayor (generalmente unos segundos).

La velocidad de un programa compilado estriba en que se calcula de antemano toda la información posible sobre el código fuente que se va a ejecutar. Por eso un compilador necesita obtener determinada información sobre el programa durante la fase de compilación (por ejemplo, el tipo de dato que almacena una variable, si es numérica o de cadena, dónde se va a guardar en memoria, etc). Un intérprete no necesita esto ya que lo hace durante la ejecución. Por eso hay cosas que un compilador no puede hacer y un intérprete sí. El ejemplo más claro es el de la función VAL del Sinclair BASIC. Es una instrucción muy potente. LET a$ = "x+x": LET b = VAL a$ almacena en la variable b el resultado de la expresión "x+x". Dado que la variable a$ puede cambiar su valor durante la ejecución del programa, es imposible saber en tiempo de compilación que tipo de dato se va a guardar en b (un valor de punto flotante, un número entero, etc) ni cómo calcularlo. De hecho, muy pocas implementaciones de BASIC tienen esta potencia, ni siquiera en la actualidad. En las competiciones de BASIC entre distintas marcas de micros antes mencionadas, el ZX perdía en velocidad pero por contra la capacidad de VAL resultaba devastadora.

En definitiva, esto era posible porque el BASIC de la ROM cada vez que evalúa una expresión le hace un análisis (con la consiguiente lentitud): se gana versatilidad, pero se pierde velocidad.

Los compiladores modernos (y ZX BASIC lo es) se basan en capas:

  • La primera capa es la de análisis de código (que a su vez se suele descomponer en dos: una capa de análisis léxico y otra de análisis sintáctico). Esta capa intenta comprender el programa y verificar que la sintaxis es correcta construyendo una representación en memoria del programa llamada Árbol Sintáctico Abstracto (AST en inglés).
  • La segunda capa es la de análisis semántico, que realiza una primera traducción y además algunas comprobaciones extra que la capa anterior no puede realizar. También se realizan las primeras optimaciones de código. En general, lo que hace es traducir el programa a un ensamblador ficticio llamado código intermedio. Esta capa y la anterior suelen ir juntas en el código (muchas veces son la misma capa en realidad) y se les llama frontend.
  • Este código intermedio será nuevamente traducido a ensamblador de la arquitectura de destino (en nuestro caso Z80). Al contrario que el lenguaje BASIC, el código intermedio es muy rígido y muy fácil de analizar. No es necesario crear otro traductor para esta fase. A esta capa y a las que siguen se las suele llamar backend.
  • Finalmente, un ensamblador (el ZX BASIC contiene uno propio) y un enlazador de código objeto (el ZX BASIC no usa ninguno: por ahora todo se trabaja en ensamblador y se compila a binario directamente) realizan el resto del trabajo produciendo el binario final.
Frontend Análisis Léxico Convertir letras a palabras y símbolos
Análisis Sintáctico Comprobar la sintaxis y las frases
Análisis Semántico Comprobar los tipos de variable, declaraciones duplicadas o fuera de contexto. Construcción del Árbol Sintáctico.
Optimización del Árbol
Generación de Código Intermedio
Backend para ZX Spectrum Traducción a Ensamblador (Z80)
Optimización de Código Ensamblador (reordenación de registros, etc)
Ensamblado: Traducción a Código Máquina (o Código Objeto)

Prácticamente todos los compiladores actuales trabajan de forma similar. La ventaja de esto es que se pueden cambiar las capas de backend de manera que es posible compilar el mismo programa para distintas arquitecturas: se podría hacer que un programa en ZX BASIC compilara para Windows, Linux, Nintendo DS, teléfono móvil, PlayStation o cualquier otra plataforma. Evidentemente cada plataforma tiene su limitación, pero dado que el ZX es la más limitada de todas sin duda, esto no debería suponer problema alguno. Los compiladores de este tipo se llaman retargeable compilers (compiladores reorientables).

También se podría cambiar el frontend. Podrías crear un traductor para otro lenguaje (C, PASCAL, LOGO...) que pasara a código intermedio, y usar el mismo backend. Esta filosofía es la que usa .NET, por ejemplo, donde distintos lenguajes fuentes compilan a CIL (Common Intermediate Language) que es luego interpretado por el .NET Framework que tengas instalado en Windows (si es que usas ese sistema operativo).

Resumiendo: ZX BASIC es un compilador cruzado reorientable de 3 capas.

Instalación

El compilador ZX BASIC está hecho en Python (un lenguaje interpretado de scripting). Esto lo hace portable a todas aquellas plataformas donde esté python (actualmente incluso para telefonos Nokia con Symbian, y para Windows Mobile). Es posible en teoría compilar programas en ZX BASIC para nuestro Spectrum en cualquier sitio donde esté python. Lo único que se requiere es que la versión de Python instalada sea igual o superior a la 2.5. En el caso de Windows, además, hay una versión instalable (.MSI) que ni siquiera requiere python. Si usas Windows y tienes dudas, te recomiendo que uses esta versión.

Puedes descargarte la última versión del compilador desde http://www.boriel.com/files/zxb/ . Verás que hay varias versiones. Descárgate siempre la más reciente. Hay un alias llamado latest version que siempre apunta a la última versión (en el momento de escribir esto, la 1.0.3). El archivo con extensión .MSI es el archivo de instalación de Windows antes mencionado. Si no estás familiarizado con todo lo que has leído hasta ahora, usa esta versión.

Para Linux y otras plataformas, descomprime la versión .zip (o .tar.gz, la que te sea más cómoda) en algún sitio de tu PATH. Para desinstalar el compilador, sólo tienes que borrar la carpeta donde lo has descomprimido. Si usaste la versión .MSI de windows, desinstálalo desde el Panel de Control (Agregar o Quitar Programas).

Para comprobar que se ha instalado correctamente, abre una consola y teclea zxb.py --version. Si usaste la versión autoinstalable de Windows, abre una ventana nueva de consola de comandos MS-DOS porque las variables de entorno han cambiado, y teclea: zxb --version (fíjate que no tiene la extensión .py). En ambos casos, se debería imprimir la versión del compilador (1.0.5).

A partir de ahora todos los ejemplos usarán la orden zxb (usa zxb.py si instalaste el compilador descomprimiéndolo en una carpeta).

Nuestro primer programa

El ritual a la hora de probar un nuevo lenguaje es el programa Hola Mundo, que básicamente imprime ese mensaje por pantalla. Vamos a hacer lo mismo, para familiarizarnos con todo el proceso de creación y compilación de un programa.

El código fuente de ZX BASIC tiene que estar en un archivo de texto ASCII. Usa tu editor de texto favorito, y escribe el siguiente programa:


  10 PRINT "Hola Mundo!"

Como puedes ver, es una sentencia BASIC de toda la vida. Ahora graba el programa como hola.bas. Nunca grabes tus programas en el directorio de instalación del compilador. Hazlo en un directorio propio y evita que tu código se mezcle con el del compilador. De esa forma, cuando salgan nuevas versiones no tendrás problemas borrando el directorio del compilador para instalar la nueva versión.

Ahora abre una consola de comandos y posiciónate en el directorio donde has grabado el programa hola.bas. Vamos a realizar la compilación:

zxb -T -B -a hola.bas

Te recomiendo que dejes de leer aquí y hagas todo esto en el computador antes de seguir. Si todo ha ido bien no aparecerá nada por pantalla. Se tiene que haber creado el programa hola.tzx (que es tu programa compilado en formato .tzx) en el mismo directorio donde tienes el código fuente hola.bas. Este programa se puede ejecutar directamente en cualquier emulador (yo utilizo EMUZWIN) que admite el formato .TZX. También hay programas que convierten los archivos .TZX a archivos de sonido .MP3 que son los que se usarían para cargar en un ZX Spectrum real. De todas formas se pueden generar otros formatos.

Recuerda que tu programa es compilado en código máquina. Normalmente el ZX Spectrum necesita un cargador en BASIC que cargue el código máquina y lo ejecute. Si cargas el programa a velocidad normal verás que primero aparecerá en pantalla Program: "hola.bas". Es el cargador en BASIC. Este cargador a continuación cargará el código máquina y lo ejecutará con un RANDOMIZE USR. Si interrumpes la carga verás que es un cargador muy simple y sin ninguna protección y que el código máquina de tu programa se carga y ejecuta a partir de la dirección de memoria 32768 (8000h en hexadecimal).

Si todo esto te ha funcionado, enhorabuena: ya estás listo para empezar a programar. ZX BASIC intenta ser lo más parecido al Sinclair BASIC, de manera que te sea cómodo empezar a programar si ya conoces éste último. Sin embargo, existen algunas limitaciones por ser un programa compilado (como ya se comentó antes). Algunas instrucciones como VAL no se comportan igual. Otras no existen porque no tienen sentido (LIST, LLIST). Y algunas otras están expandidas o admiten parámetros extra para aprovechar la potencia que ahora tenemos. Además, hay instrucciones nuevas que te ahorrarán trabajo: sentencias de bucles más potentes y subrutinas con variables privadas son sólo un ejemplo de ello.

Parámetros de compilación

El compilador ZX BASIC, por defecto, lee el código fuente y genera un archivo binario con la extensión .bin que contiene el código máquina (es decir, los bytes del código objeto directamente). Este código máquina no es reubicable, y tiene que ser cargado a partir de la dirección 32768. Como puedes suponer, esto puede ser de poca utilidad si no disponemos de algún método para pasar este código máquina a un emulador o un ZX Spectrum real. Además, puede que queramos que nuestro código empiece en la dirección 28000 (para ganar algo más de memoria que tan escasa es en nuestro ZX). Todo esto y mucho más se puede cambiar con las opciones de compilación.

ZX Basic tiene bastantes parámetros. Sólo veremos los más importantes. Si tecleas zxb -h tendrás una escueta ayuda. Puedes buscar más información en la Wiki del compilador, que está en http://www.boriel.com/wiki/en/index.php/ZXBasic en inglés. Los parámetros tienen una versión larga que empieza con doble guión (por ej. --tzx) y una versión corta equivalente (en este caso, -T). Los parámetros más importantes son:

  • -T o --tzx hace que el formato de salida sea un archivo de cinta .tzx
  • -t o --tap hace que el formato de salida sea un archivo de cinta .tap. Esta opción y la anterior son excluyentes.
  • -B o --BASIC hace que se genere un cargador BASIC que cargue nuestro programa. Si usas esta opción, necesariamente tienes que haber usado una de las anteriores.
  • -a o --autorun hace que nuestro programa se ejecute automáticamente tras ser cargado. Esta opción obliga a que se use la opción --BASIC, ya que se requiere un cargador BASIC para que el programa se autoejecute.
  • -o <fichero de salida>. Permite especificar un nombre de archivo distinto para el programa resultante. Generalmente se usará el mismo nombre que el programa de código fuente (.bas), pero con una extensión distinta (.bin, .tap o .tzx)
  • -A o --asm Hace que no se genere el código objeto. En lugar de eso obtendremos el código ensamblador de todo nuestro programa (un archivo ASCII con extensión .asm). Esta opción es útil si queremos optimizar a mano nuestro programa. Se ha intentado que el código ensamblador generado sea bastante legible y con comentarios incluidos.
  • -S o --ORG cambia la dirección de comienzo de ejecución del código máquina. Como se dijo antes, por defecto es 32768, pero podemos hacer que se ejecute a partir de una dirección distinta (por ej. 28000) para obtener más memoria.

El Lenguaje ZX BASIC

He intentado que el lenguaje ZX BASIC sea lo más similar posible al de Sinclair. Como ya expliqué en el apartado anterior, hay instrucciones que no tienen sentido en un programa compilado y otras que son prácticamente imposibles de implementar. Pero también es cierto que se puede expandir el lenguaje y crear nuevas instrucciones para hacer cosas con la potencia que ahora tenemos. Para llegar a un consenso, se han tomado palabras reservadas del lenguaje FreeBASIC. En general, lo mejor es ir a la Wiki del compilador ZX BASIC ya mencionada pues allí hay una referencia de las palabras clave así como multitud de ejemplos.

Algunas cosas importantes:

  • ZX BASIC distingue en entre mayúsculas y minúsculas en los nombres de variables y funciones. Una variable llamada A es diferente de otra llamada a. Sin embargo en lo que se refiere a palabras reservadas (por ejemplo PRINT o FOR), éstas se pueden escribir como se deseen. La distinción entre mayúsculas y minúsculas para los identificadores de usuario podrá ser configurable en futuras versiones del compilador, pero no actualmente.

    Ejemplo.- Las siguientes palabras reservadas son todas equivalentes

    PRINT print Print PrInT
    

  • Como en el Spectrum, el ZX BASIC está orientado a líneas: no puedes partir una línea en medio de una sentencia BASIC. Si deseas hacerlo, usa el carácter de subrayado (_), al final de la línea partida para indicar que ésta continúa en la siguiente.

    Ejemplo.- La siguiente línea partida no dará error, porque lleva un caracter de subguión (o subrayado) al final

    10 PRINT _
        "Hola Mundo"
    

  • Los números de línea del BASIC ya no son obligatorios. Son opcionales y se usan como etiquetas. También puedes usar identificadores como etiquetas. De todas maneras, si quieres puedes seguir usando líneas numeradas como en el BASIC tradicional del ZX Spectrum, si te puede la nostalgia (a mí a veces me puede, qué demonios). ¡Además, ya no están limitadas a 9999, y ni si quiera tienen por qué estar numeradas en orden!

    Ejemplo.- Un bucle infinito sin usar numeración de líneas:

    BucleInfinito: REM Esto es una etiqueta.
            PRINT "Hola Mundo" : REM Esto es una línea sin numeración
            GO TO BucleInfinito : REM Esto podría ser GO TO 10 en Sinclair BASIC
    

Los tipos de datos

Si eres un programador de Sinclair BASIC con cierta experiencia seguramente sabrás que el BASIC del ZX Spectrum trabaja siempre en punto flotante. El formato de dicha representación puede resultar complejo y ocupa 5 preciosos bytes (una mantisa de 32 bits y un exponente de 8 en exceso 127, tal y como explica el manual del ZX). Eso se traduce en un coste enorme en tiempo y en memoria. Si solo queremos almacenar valores enteros menores que 255, podríamos usar bytes directamente. Ocuparían la quinta parte de la memoria y su procesado sería mucho más rápido (apenas una o dos instrucciones en ensamblador). Por contra, se puede perder precisión en algunos cálculos. No te preocupes: el formato de punto flotante también está disponible.

Está claro que según el uso que le vayamos a dar a una variable, ésta almacenará un tipo de dato determinado. Este tipo de dato le indica al compilador cómo tratarlo y el tamaño que ocupará en memoria. Así pues, usaremos tipos de datos y diremos que ZX BASIC es un lenguaje tipado. Los tipos de datos que maneja el ZX BASIC son:

Tipo Tamaño Clase de dato Intervalo
Byte 1 byte Entero corto con signo [-128 ... 127]
UByte 1 byte Entero corto sin signo [0 ... 255]
Integer 2 bytes Entero con signo [-32768 ... 32767]
Uinteger 2 bytes Entero sin signo [0 ... 65536]
Long 4 bytes Entero largo con signo [-2147483648 a 2147483647] (más de 2.100 millones)
Ulong 4 bytes Entero largo sin signo [0 .. 4294967295] (de 0 a más de 4.200 millones)
Fixed 4 bytes Decimal con punto fijo [-32767.9999847 .. 32767.9999847] con una precisión de 1 / 2^16 (0.000015 aprox.)
Float 5 bytes Decimal con punto flotante Como en el ZX Spectrum

Estos son todos los tipos de datos numéricos. Los enteros deberían estar claros. Además hay un tipo de decimal en punto fijo, llamado Fixed, muy interesante. Si vas a usar números decimales entre -32767 y 32767 quizá deberías usar este tipo de números (siempre y cuando sólo uses las 4 operaciones básicas: sumas, restas, productos y divisiones).

Estos tipos de datos se usan para almacenar valores y computar expresiones matemáticas. Las expresiones matemáticas usan la misma sintaxis que el Sinclair BASIC y los mismos operadores y precedencia, con la excepción del operador de potencia (^). La expresión 2^2^3 se calcula de forma distinta en un Spectrum que en la mayoría de los lenguajes. Haz la prueba... En cualqueir caso, si tienes dudas, usa paréntesis.

Variables de Cadenas de Carácteres

Al igual que Sinclair BASIC, ZX BASIC también es capaz de manejar cadenas alfanuméricas. Tradicionalmente, en el lenguaje BASIC las variables alfanuméricas se denotaban con el sufijo $. En ZX BASIC (y los BASIC modernos en general) esto es opcional. Además, Sinclair BASIC sólo permitía nombres de variables alfanuméricas de una letra. Aquí no existe esa limitación, pudiendo usar variables alfanuméricas como una_variable_con_nombre_muy_largo$.

Los valores alfanuméricos se pueden unir (concatenar) usando el operador de suma (+), al igual que en Basic de Sinclair.

Asignando Valores a Variables

Al igual que en Sinclair BASIC, se usa la sentencia LET para asignar un valor a una variable. Pero ahora ésta se puede omitir. Las siguientes líneas son equivalentes:

LET a = 1
a = 1

DIM: Declarando Variables

Hemos visto que ZX BASIC usa distintos tipos de dato. Cuando usas una variable el compilador intenta adivinar su tipo (si es alfanumérica, entera, etc.), pero en general no va a poder hacerlo. Si no sabe qué tipo asignar, usará el punto flotante como el Sinclair Basic. Esto conlleva un desperdicio de memoria y mayor lentitud. Podemos indicarle al compilador el tipo de dato de una variable declarándola. Para declarar una variable se usa la palabra reservada DIM:

REM Declaración de dos variables de tipo entero byte sin signo
DIM a, b uByte

REM Declaración de una variable en punto Flotante con valor inicializado
DIM longitud = 3.5 AS Float: REM longitud = 3.5 metros

Si vas a declarar una variable, tienes que hacerlo antes de su primer uso.

Arrays

Los arrays se declaran como en Sinclair BASIC, y admiten tantas dimensiones como quepan en memoria. No obstante, al contrario que en Sinclair Basic, la numeración de los índices no comienza en 1, sino en 0. Además, podemos declarar opcionalmente el tipo de elemento. Veamos un ejemplo:

DIM a(10) AS Float : REM un array de 11 flotantes, del 0 al 10, ambos inclusive

Podemos trabajar normalmente como en Sinclair BASIC empezando desde índice uno, pero desperdiciaremos la posición 0. No obstante, también podemos declarar el índice mínimo y máximo del array de forma explícita:

DIM a(1 TO 10, 1 TO 5) AS Float : REM  esto es equivalente a DIM a(10, 5) en el BASIC de ZX Spectrum

Si queremos que por defecto los arrays comiencen en 1 (como en Sinclair BASIC) y no en 0, hay que usar la opción --array-base=1 al invocar al compilador.

Al contrario que en el BASIC de Sinclair, el nombre de las variables de array puede tener cualquier longitud (estaban limitados a una sola letra en el Spectrum, al igual que las variables de bucles FOR y las alfanuméricas). Existen muchas más cosas que se pueden hacer (como declarar arrays inicializados) pero no las veremos en este artículo. Pregunta en el foro de speccy.org o en del compilador, o mejor consulta la Wiki.

Es posible declarar arrays con valores inicializados. Esto es útil porque no existen ni READ, ni DATA, ni RESTORE. Lo haríamos así:

REM Definimos un array de 2 filas y 8 columnas
DIM MiUdg(1, 7) AS uByte => { {60, 66, 129, 129, 129, 129, 66, 60}, _
                              {24, 60, 60, 60, 126, 251, 247, 126}}

Observa el caracter de subrayado "_" al final de la primera línea del array, ya que hay que partirla (si no, quedaría muy larga).

Sentencias de Control de Flujo

GO TO, GO SUB, RETURN

Idénticas al BASIC de Sinclair, aunque se desaconseja su uso. GO TO puede escribirse junto, como GOTO. Ídem para GOSUB. Se pueden usar números de línea o etiquetas como vimos en un ejemplo anterior.

FOR

La sentencia para realizar bucles FOR se comporta de la misma forma que en ZX Basic. No obstante, al ser un lenguaje compilado, tienes que tener en cuenta algunas cosas. Por ejemplo, hacer un bucle FOR usando una variable en punto flotante es bastante más lento que hacerla con una de tipo entero. Su uso es prácticamente igual al del ZX Spectrum, pero hay algunas diferencias:

  • Los nombres de las variables de bucle FOR pueden tener más de una letra (no así en Sinclair BASIC)
  • El nombre de la variable se puede omitir en las sentencias NEXT. O sea, se puede escribir FOR x = 1 TO 10: NEXT : REM se omite la 'x' en NEXT
  • No se puede poner un NEXT en cualquier parte del programa. Tiene que ser después de un FOR. Para bucles anidados (uno dentro de otro) el NEXT interno tiene que referirse obligatoriamente al bucle más interno. Ídem para los restantes bucles.

Existen además, dos sentencias de control de bucles nuevas:

  • EXIT FOR termina el bucle FOR y salta justo al final (solíamos usar on GOTO para esto, ¿verdad?)
  • CONTINUE FOR que "continúa" el bucle, es decir, realiza un NEXT. En el BASIC de Sinclair poníamos un NEXT <variable>, pero en ZX BASIC esto no se permite. Hay que usar CONTINUE.

IF

Esta sentencia sí difiere del BASIC tradicional de Sinclair. Es más potente. Admite varias líneas después del THEN, y además incluye la cláusula ELSE ("en otro caso"). Además es necesario terminarla con un END IF. Un ejemplo:

IF a < b THEN
    PRINT "a es menor que b"
    PRINT "Esto es otra sentencia más"
ELSE : REM si no...
    PRINT "a no es menor que b"
END IF

En Sinclair BASIC teníamos que ingeniárnoslas haciendo algo así:

1000 IF a < b THEN PRINT "a es menor que b": PRINT "Esto es otra sentencia más" : GO TO 1030
1010 REM si no...
1020 PRINT "a no es menor que b"
1030 ...

WHILE

Esta sentencia es nueva ZX BASIC y sirve también para hacer bucles. Es más potente que FOR porque el bucle se repite mientras se dé la condición que se indique. Si al empezar el bucle la condición es falsa, entonces no se llega a ejecutar ninguna iteración. Un ejemplo:

WHILE a < 10
    LET a = a + 1
END WHILE

La finalización del bucle tiene que terminarse con END WHILE o con WEND (son equivalentes). Al igual que con FOR, las sentencias EXIT WHILE y CONTINUE WHILE también pueden utilizarse con WHILE para terminar el bucle o continuar con la siguiente iteración.

DO ... UNTIL

Similar a la anterior, pero aquí la comprobación de la condición se hace al final del bucle y éste se repite mientras no se cumpla la misma (es decir, mientras sea falsa). Al contrario que con WHILE, el bucle se ejecutará al menos una vez. Un ejemplo:

DO
    LET a = a + 1
LOOP UNTIL a >= 10

Igualmente podemos usar CONTINUE DO y EXIT DO para adelantar el bucle o terminarlo anticipadamente.

DO ... WHILE

Existe también la construcción DO ... WHILE, idéntica a la anterior, solo que esta repite el bucle mientras la condición se cumpla.

Manejo de Memoria

PEEK y POKE

Son idénticas al Sinclair BASIC, pero ahora, además, está extendidas. Tanto PEEK como POKE admiten especificar el tipo de dato que se guarda en memoria. Por defecto será de tipo byte sin signo (como en el ZX Spectrum). Así pues, las siguientes dos líneas son equivalentes:

LET a = PEEK 16384
LET a = PEEK(uByte, 16384)

Pero a veces queremos guardar o leer un entero de 16 bits. En el mismo manual del ZX Spectrum viene un ejemplo. Estos valores, en el Z80 se leen de esta manera:

REM Guardamos en la variable 'a' la dirección de comienzo de los GDU
LET a = PEEK 23675 + 256 * PEEK 23676 : REM Forma tradicional
LET a = PEEK(Uinteger, 16384)

Ambas formas son equivalentes, pero la segunda es más eficiente (por el ensamblador generado) y más legible. Si se usa la segunda forma, los paréntesis son obligatorios. Igualmente, podemos guardar con POKE valores de más de un byte:

REM Cambiamos la dirección de los GDU según la variable 'a'

REM Forma tradicional
POKE 23675, a - INT(a / 256) * a : REM cuidado con el redondeo si a es entera.
POKE 23676, INT(a / 256)

REM Forma moderna
POKE Uinteger 23675, a

Claramente, la segunda forma es más legible (y preferible). Además, se traduce de forma más eficiente a ensablador. ¡Ahora ya es posible (y extraño), guardar números flotantes con POKE en memoria! Prueba a hacer POKE Float 16384, PI.

Salida por Pantalla

PRINT

Se ha intentado que PRINT sea lo más compatible posible con el original. De hecho, a nivel sintáctico funciona igual. E incluso los códigos de color de la ROM se pueden utilizar. Esta implementación de PRINT no usa la rutina de la ROM (para mayor velocidad) sino que es propia, y permite imprimir en todas las filas de pantalla. Así pues, PRINT AT 22,0; es una sentencia legal. No existen canales.

BORDER, PAPER, INK, INVERSE, BRIGHT, FLASH, OVER

Funcionan igual que en Sinclar BASIC. Para BORDER, usar un color mayor que 7 se suele ignorar, ya que sólo se toman los 3 bits más bajos. Para INK y PAPER si se puede usar el valor 8 (transparencia). Over tiene funcionalidades extra: OVER 1 actúa como en el ZX Spectrum e imprime realizando la operacion XOR. Pero se puede usar también OVER 2 y OVER 3 en conjunción con el comando PRINT. OVER 2 realiza un AND y OVER 3 realiza un OR. Esto se puede usar para crear efectos de filmation. Prueba el siguiente ejemplo de BORDER y compara su velocidad con la del Basic de la ROM:

10 BORDER 0: BORDER 1: GOTO 10

PLOT, DRAW y CIRCLE

Funcionan igual que en el Basic original... pero más rápido. PLOT usa la rutina de la ROM, por lo que se aplican todos atributos de color que usa el comando PLOT original. La diferencia ahora es que disponemos de los 192 puntos de pantalla para pintar. La coordenada (0, 0) es ahora la esquina inferior izquierda física de la pantalla. Eso significa que si usamos un comando de dibujado cualquiera, nuestros dibujos aparecerán 16 píxeles más abajo que en el Sinclair BASIC original, pues ahora disponemos de 16 líneas de altura más.

DRAW y CIRCLE están optimizadas (no son las de la ROM) y emplean el algoritmo de Bresenham, por lo que son algo más rápidas (especialmente CIRCLE). También se pueden dibujar arcos, con DRAW x, y, a como en el BASIC original. La rutina está copiada de la ROM, para simular el mismo comportamiento que la original, pero modificada para dibujar también en toda la pantalla, como las anteriores. El siguiente ejemplo está sacado del manual del ZX Spectrum (el reloj), pero está escrito con la nueva sintaxis, sin usar números de línea:

REM Del manual de ZX Spectrum 48K
REM Un programa de Reloj

REM Primero dibujamos la esfera
CLS
FOR n = 1 to 12
    PRINT AT 10 - (10 * COS(n * PI / 6) - 0.5), 16 + (0.5 + 10 * SIN(n * PI / 6)); n
NEXT n

REM Lo siguiente sería PRINT #0; en el Basic de la ROM
PRINT AT 23, 0; "PULSA UNA TECLA PARA TERMINAR";

FUNCTION t AS ULONG
    RETURN INT((65536 * PEEK (23674) + 256 * PEEK(23673) + PEEK (23672))/50)
END FUNCTION

DIM t1 as FLOAT

OVER 1
WHILE INKEY$ = ""
    LET t1 = t()
    LET a = t1 / 30 * PI: REM a es el ángulo del segundero en radianes
    LET sx = 72 * SIN a : LET sy = 72 * COS a
    PLOT 131, 107: DRAW sx, sy

    LET t2 = t()
    WHILE (t2 <= t1) AND (INKEY$ = "")
        let t2 = t()
    END WHILE : REM Espera hasta el momento de moverlo

    PLOT 131, 107: DRAW sx, sy
END WHILE

SCREEN$, ATTR, POINT

Existen y se comportan como en Sinclair BASIC. Pero son funciones externas. Las funciones externas son aquellas que existen en un fichero .BAS aparte. En concreto, en el directorio library/ del compilador hay una biblioteca de funciones que irá creciendo con el tiempo. Para usarlas en tu programa, tienes que usar una directiva de preprocesador como la que sigue:

#include <screen.bas>

Los ficheros que las contienen son SCREEN.BAS, ATTR.BAS y POINT.BAS respectivamente. Si miras en ese directorio, verás que hay otras funciones. Enseguida veremos cómo definir nuestras propias funciones.

Sonido

BEEP

Por ahora, el único soporte de sonido es el comando BEEP, que usa la rutina de la ROM y es idéntico al de Sinclair BASIC. La única ventaja es que aquí, al disponer de mayor rapidez, podemos implementar algunos trucos de sonido. Existe un comando en la versión 128K del BASIC, PLAY que se espera poder implementar en futuras versiones.

El siguiente ejemplo está sacado del manual el ZX Spectrum (Frere Gustav del manual de ZX Spectrum 48K, capítulo 19):


10 PRINT "Frere Gustav"
20 BEEP 1,0: BEEP 1,2: BEEP .5,3: BEEP.5,2: BEEP 1,0
30 BEEP 1,0: BEEP 1,2: BEEP .5,3: BEEP.5,2: BEEP 1,0
40 BEEP 1,3: BEEP 1,5: BEEP 2,7
50 BEEP 1,3: BEEP 1,5: BEEP 2,7
60 BEEP .75,7: BEEP .25,8: BEEP .5,7: BEEP .5,5:BEEP .5,3:
       BEEP.5,2: BEEP 1,0
70 BEEP .75,7: BEEP .25,8: BEEP .5,7: BEEP .5,5: BEEP .5,3: BEEP .5,2:
       BEEP 1,0
80 BEEP 1,0: BEEP 1,-5: BEEP 2,0
90 BEEP 1,0: BEEP 1,-5: BEEP 2,0

Funciones Matemáticas y Números aleatorios

STR$, VAL, PI, SIN, COS, TAN, ASN, ACS, ATN, EXP

Estas funciones trabajan todas igual que en el BASIC original del ZX Spectrum a excepción de VAL. Como ya se dijo antes, VAL es un caso particular prácticamente imposible de compilar. Funciona como en la mayoría de los BASIC estándar: se convierte una cadena alfanumérica a punto flotante, pero esta cadena sólo puede contener un número (no una expresión). Si una cadena no se puede convertir, se devuelve 0. Luego VAL "x+x" devolverá 0 siempre, aunque la variable x esté definida.

RANDOMIZE, RND

Existen y se usan como en el ZX Spectrum con la diferencia de que la rutina de números aleatorios es más rápida ya que no usa la calculadora de punto flotante de la ROM sino registros y desplazamientos en ensamblador. Esta rutina es un generador lineal congruente (como la del ZX Spectrum) pero usa números de 32 bits, por lo que tiene un periodo de miles de millones (antes de que vuelva a repetirse la secuencia). Se ha sacado del libro Numerical Recipes in C (disponible gratuitamente en internet, aunque vale la pena comprarlo). Además, tiene una mejor dispersión. Prueba el siguiente programa en el ZX Spectrum, tanto en el BASIC de la ROM como en ZX BASIC (compilado):

10 LET x = INT(RND * 256): LET y = INT(RND * 175)
20 PLOT x, y
30 GOTO 10

Aparte de la velocidad del programa compilado, notarás que en la versión del BASIC de Sinclair aparecen líneas diagonales. Ello indica que la aleatoriedad de los números no es tan alta en el BASIC de la ROM como se podría esperar.

Entrada y salida

INKEY$, IN, OUT

También están presentes y funcionan de forma idéntica a la de Sinclair BASIC... excepto por su velocidad, que es bastante mayor en el caso de IN y OUT.

Caracteres gráficos, GDU y códigos de color

Para poder introducir caracteres gráficos y códigos de color, se ha seguido el mismo convenio que BASIN (un entorno para programar y depurar programas BASIC del ZX Spectrum). Así, dentro de una cadena de caracteres, el caracter de barra invertida '\' tiene un significado especial. Así, para escribir un el GDU "A", escribiremos PRINT "\A". Si queremos escribir varios carácteres gráficos seguidos, "AB", escribiremos: PRINT "\A\B". Si queremos imprimir la barra invertida (este carácter existe en el Spectrum), usaremos doble barra: PRINT "\\"

Los símbolos gráficos (aquellos que tenían formas de cuadraditos y que se sacaban en con el cursor en modo gráfico y pulsando un número del 0 al 8) también pueden ponerse como en BASIN. Prueba a poner PRINT "\:.\'.". Como puedes ver, se componen de la barra invertida seguido de dos caracteres que pueden ser uno de estos: [ ][.]['][:] (espacio en blanco, punto, apóstrofe y dos puntos). Cada "puntito" del caracter representa un cuadrado del gráfico (es difícil de explicarlo, lo mejor es que lo pruebes). Estos códigos son leídos por el compilador y reemplazados por un solo byte, correspondiente al código ASCII del caracter que queramos representar. Puedes probar a hacer un programa BASIC en Basin y grabarlo como .bas (ASCII) y ver la secuencia de caracteres generada.

Igualmente, los códigos de color, brillo, flash e inversión de vídeo pueden especificarse "en línea", como se hacía con el ZX Spectrum, siguiendo el mismo convenio que BASIN. Para escribir por ejemplo, HOLA con tinta roja sobre fondo negro, puedes hacer: PRINT "{i2}{p0}HOLA"

Los códigos son:

Código Significa Valores
{iN} Tinta N = 0..7
{pN} Papel N = 0..7
{fN} Flash N = 0..1
{vN} Inverse N = 0..1
{bN} Brillo N = 0..1

Además, puedes especificar cualquier caracter ASCII en decimal, usando \#xxx. Por ejemplo, el copyright puedes ponerlo como PRINT "\*" o bien como PRINT "\#127"

Funciones y Subrutinas

Una de las grandes diferencias del ZX BASIC respecto a sinclair BASIC es la posibilidad de definir funciones y subrutinas, y que además estas contengan variables privadas. La única posibilidad que ofrecía el BASIC de Sinclair para definir funciones era con DEF FN, que aquí ya no existe. Para definir una función, usaremos la palabra reservada FUNCTION, así:

FUNCTION mifuncion(x AS Integer, y AS Byte) AS Integer
    REM Función que devuelve x + y
    RETURN x + y
END FUNCTION

Esta pequeña función recibe 2 parámetros, un entero de 16 bit en x y un byte en y, para devolver la suma de ambos (x + y).

Para usar la función, sólo tienes que llamarla como como si fuera una función BASIC cualquiera:

PRINT mifuncion(3, 5) : REM Imprime 8

Mira el ejemplo anterior del reloj, cómo define y usa una función.

También se puede llamar a una función sin más, como si fuera una subrutina:

mifuncion(3, 5) : REM suma 3 + 5 y luego los descarta y no hace nada con el resultado

Al igual que las funciones, también puedes definir subrutinas. Se definen usando la palabra reservada SUB. La sintaxis es muy similar a la anterior:

SUB misubrutina(x AS Integer, y AS Byte)
    REM Función que devuelve x + y
    PRINT x + y
END SUB

Y la invocamos así

misubrutina(3, 6) : REM imprime 9

La diferencia principal es que SUB siempre tiene que invocarse. Nunca devuelve un resultado, al contrario que las funciones. De hecho, no puedes usar RETURN <valor> para salir de una subrutina, sino simplemente RETURN. Retornar de una subrutina o función y retornar de un GOSUB son cosas distintas. Al escribir simplemente RETURN, desde dentro de una función o subrutina siempre se supondrá que se retorna de la misma y no de un GOSUB.

Esto es así porque cada vez que se retorna, el programa tiene que hacer ciertas gestiones con la pila de código máquina antes de regresar. Así pues, no es lo mismo retornar de un GOSUB, que de una función o subrutina. Al declarar una subrutina ya le estamos indicando al compilador que no es necesario devolver ningún valor, y eso permite hacer una pequeña optimización.

Por último, las variables declaradas (DIM) dentro de una subrutina o función son privadas y no son accesibles desde fuera. Además, sólo usan memoria durante la ejecución de la función. Al salir de la misma, esa memoria se libera (se usa la pila de código máquina). Mira este ejemplo:

SUB Ejemplo
    DIM a as Integer: REM esta variable es privada

    LET a = 5
    PRINT "En la subrutina, a="; a
END SUB

LET a = 10
PRINT "Antes de la subrutina, a="; a
Ejemplo() :  REM  llamamos a la subrutina ejemplo e imprime la variable privada
PRINT "Despues de la subrutina, a ="; a

ASM integrado

Es posible meter líneas de ASM directamente. Esto es una característica de muy bajo nivel, y se sale un poco de los propósitos de este artículo. Básicamente, se puede incrustar ASM directamente en el código así:

ASM
...
...
END ASM

Como ZX Basic aspira a ser multiplataforma (reorientable), el código que hagas con ASM directo sólo compilará en la arquitectura que soporte ese ensamblador (en nuestro caso, Z80). Aquí tienes un ejemplo:

ASM
ld a, 0FFh
ld (16384), a
END ASM

La idea de usar ASM directamente es en aquellos bucles que requieran mucha velocidad (por ejemplo, alguna subrutina de algún juego). Si miras la biblioteca de funciones, library/ verás que muchas funciones están definidas en ensamblador.

Lo que falta...

No todo iba a ser perfecto. ZX Basic, por ser compilado, tiene cosas que le faltan. Por ejemplo, no existe INPUT. Te la tendrás que implementar (la mayoría de los juegos se implementan su propio INPUT; fíjate en El Hobbit). Ya hay una función INPUT implementada en en la biblioteca de funciones, por si te interesa esa. Prueba a usarla.

REM Importamos la función input
#include <input.bas>

LET a$ = input(32): REM Input de una cadena de 32 caracteres como máximo
PRINT a$

Es muy difícil utilizar el INPUT de la ROM, que usa canales (que ya no existen aquí), y otras zonas del área de BASIC que pueden corromper el programa compilado o la pila de ejecución (aunque se supone que el CLEAR del cargador evitará esto). Tampoco tienen sentido READ, DATA y RESTORE. Son un desperdicio de memoria e imposibles de compilar tal y como las ofrece el BASIC de la ROM; aunque es probable que se implementen en un futuro con ciertas limitaciones.

Ejemplos

Este artículo está quedando muy extenso, así que lo cortaré aquí (si fuera para MicroHobby, daría para varios números). Quedan algunas cosas por explicar (como el alias de variables o las instrucciones de manejo de bits: rotaciones, etc.) En cuaquier caso, en el directorio examples tienes varios ejemplos que puedes compilar para aprender cómo funcionan. El ejemplo de la Bandera Inglesa está sacado del manual del ZX Spectrum y funciona tal cual está, sin ninguna modificación. Otros ejemplos, como el Snake (cortesía de Federico J. Alvarez Valero, año 2003) se han retocado para declarar explícitamente el tipo de dato de algunas variables (recordemos que trabajar con enteros es más rápido que punto flotante), y terninar los IF con sus END IF correspondientes.

Y pasaron los años...

¿20 años? ¡Glup! Al menos se cumplió una parte del sueño (la otra no os la digo...). ¿Fue demasiado tarde? No lo creo. Al menos no en parte.

El ZX Spectrum, los micros en general, y su época de alguna manera dejaron huella. Muchos pensamos que esto fue irrepetible. En aquella época la tecnología no avanzaba tan rápidamente como ahora y es por eso que micros como el ZX Spectrum duraron de media una década sin sufrir grandes cambios. Por contra las videoconsolas y sistemas actuales apenas pasan de los 3 ó 4 años de vida.

Hoy día estamos tan saturados y acostumbrados a los cambios que ya nada nos asombra (o al menos a mí). Miles de millones de píxeles en miles de millones de colores con un sonido envolvente no consiguen dejar esa impronta que una máquina tan simple logró en apenas unas horas. Quizá porque fue la primera, no lo sé. Pero me consta que las generaciones actuales no sienten esa pasión y ese tirón por las máquinas. La PS3, la XBOX, el iPhone o el PC. Ninguno consigue enganchar tanto a los chicos de hoy día.

Suena un poco nostálgico, lo sé, pero creo que hemos tenido la suerte de haberlo podido vivir.


Enlaces y ficheros



Boriel
 
Volver arriba
> ÍNDICE DE REVISTAS <
2003-2009 Magazine ZX