Contents

  1. Q-emuLator 4.1 Q-emuLator blog • 18 hours ago
  2. DJ’s Sinclair QL Blog is back Dilwyn Jones Sinclair QL Blog • 11 days ago
  3. El porqué de los modos 512×256 y 256×256 QBlog (es) • 2 months ago
  4. MyLISP para Sinclair QL: cómo acabé escribiendo mi propio LISP (y por qué tiene la culpa una calculadora) QBlog (es) • 2 months ago
  5. Crónicas RU Sinclair QL y Compatibles 2026 QBlog (es) • 4 months ago
  6. ZX2SB: Cambio de idea de los temas especiales Old 8 Bits (es) • 4 months ago
  7. ZX2SB: Mejorando el sistema de evaluación de expresiones y cambios menores Old 8 Bits (es) • 5 months ago
  8. ZB2SB. Conversor de Basic de los ZX al Super Basic del QL. Indice Old 8 Bits (es) • 5 months ago
  9. ZX2SB: Otra vuelta atrás Old 8 Bits (es) • 5 months ago
  10. Pistorm para Sinclair QL QBlog (es) • 5 months ago
  11. ZX2SB: El generador de código SuperBASIC Old 8 Bits (es) • 5 months ago
  12. ZB2SB. Paso 3.2. Lexer. Planteamiento Old 8 Bits (es) • 5 months ago
  13. ZX2SB. Cambios en el planteamiento Old 8 Bits (es) • 5 months ago
  14. ZX2SB: El Analizador semántico Old 8 Bits (es) • 5 months ago
  15. ZX2SB: El Parser o Analizador Sintáctico Old 8 Bits (es) • 5 months ago
  16. ZX2SB. Vuelta a empezar Old 8 Bits (es) • 5 months ago
  17. ZB2SB. Paso 3.1. Lexer. Estructura de datos para manejo de Tokens Old 8 Bits (es) • 6 months ago
  18. ZB2SB. Paso Auxiliar 1. Estructuras de datos Old 8 Bits (es) • 6 months ago
  19. ZB2SB. Paso 2. Director del proyecto Old 8 Bits (es) • 6 months ago
  20. ZB2SB. Paso 1. Definir el Lenguaje de origen Old 8 Bits (es) • 6 months ago
  21. ZB2SB. Conversor de Basic de los ZX al Super Basic del QL (Reinicio) Old 8 Bits (es) • 6 months ago
  22. Sinclair QL • Versión de Old Tower para QL Foro Speccy.org (es) • 7 months ago
  23. Q-emuLator 4.0 for Windows Q-emuLator blog • 8 months ago
  24. QL desconocido: Sampled Sound System QBlog (es) • 9 months ago
  25. Sinclair QL desconocido: QL y MIDI QBlog (es) • 10 months ago
  26. Sinclair QL • Nuevo juego para Sinclair QL: The Curse of Rabenstein Foro Speccy.org (es) • 11 months ago
  27. Sinclair QL • Nuevo juego para Sinclair QL: Android 2 Foro Speccy.org (es) • 11 months ago
  28. DOOM para Sinclair QL QBlog (es) • 12 months ago
  29. Microdeal y el QL: historia de un fracaso QBlog (es) • 12 months ago
  30. RenumQB Dilwyn Jones Sinclair QL Blog • over a year ago
  31. Dracula Dilwyn Jones Sinclair QL Blog • over a year ago
  32. Q-Liberator v3.46 Update Dilwyn Jones Sinclair QL Blog • over a year ago
  33. Minerva and Games Q-emuLator blog • over a year ago
  34. Sinclair QL • Nuevo juego para Sinclair QL: Batman Foro Speccy.org (es) • over a year ago
  35. BatmanQL – Joan Gayón (2025) QBlog (es) • over a year ago
  36. BatmanQL – Descarga/Download QBlog (es) • over a year ago
  37. QL Forum Move Dilwyn Jones Sinclair QL Blog • over a year ago
  38. Blog Paused Dilwyn Jones Sinclair QL Blog • over a year ago
  39. Rudolph Is Ill Dilwyn Jones Sinclair QL Blog • over a year ago
  40. QaLendar 2025 Dilwyn Jones Sinclair QL Blog • over a year ago
  41. ZB2SB. Conversor de Basic de los ZX al Super Basic del QL (I): Introducción Old 8 Bits (es) • over a year ago
  42. ZB2SB. Conversor de Basic de los ZX al Super Basic del QL (III): Analizadór léxico segunda parte: Extractor de Tokens Old 8 Bits (es) • over a year ago
  43. ZB2SB. Conversor de Basic de los ZX al Super Basic del QL (II): Analizadór léxico primera parte, lectura de líneas Old 8 Bits (es) • over a year ago
  44. Programación del Sinclair QL (XXV): Arboles. Pruebas de los nodos Old 8 Bits (es) • over a year ago
  45. Programación del Sinclair QL (XXII): Variables Old 8 Bits (es) • over a year ago
  46. Programación del Sinclair QL (XXIV): Arboles. Estructura de los nodos en Super Basic Old 8 Bits (es) • over a year ago
  47. Programación del Sinclair QL (XXIII): Punteros Old 8 Bits (es) • over a year ago
  48. Programación del Sinclair QL (XIX): Estructura de datos. Arbol binario de Búsqueda Old 8 Bits (es) • over a year ago
  49. NI CVI/LabVIEW and monitors with different scaling Kilgus.net - blog • over a year ago
  50. Retrowiki Dilwyn Jones Sinclair QL Blog • over a year ago

Sunday, 13. September 2026

Q-emuLator 4.1

23:27 GMT (18 hours ago)

 Q-emuLator 4.1 has a new GPU graphics engine that uses less CPU and adds optional scanline and monitor persistence effects.


Variable pixel ratio - a feature requested by many users - is finally available. It allows  MODE 4 pixels to appear square at the expense of a squished image.

   

The built-in debugger now supports the 'step out' command and allows using the mouse to toggle breakpoints and inspect stack traces.



Thursday, 03. September 2026

DJ’s Sinclair QL Blog is back

21:18 GMT (11 days ago)

This blog took a break for some time as I felt I needed a bit of a break from the QL scene, a slight burnout happened, and a couple of bereavements happened this year on top of everything else.

In theory, retirement meant I should have more time for hobbies like the Sinclair QL. In practice, as many who have recently retired will tell you, it often doesn’t work like that! People often go “oh, you’ve just retired, you’ll have time to join our committee now!” or something like that. If anything, I’ve ended up busier since retiring than I ever was.

How often will this blog get updated now? Hard to tell. Much of my QL stuff is back out of storage. Now all I need is the time to do something with it all!

One thing I won’t be doing is restoring my old QL website; that was honestly too much work and I’m not getting any younger! Since I closed it down, a few kind people on the QL scene took matters into their own hands and kindly made available a copy of the last version of the site, minus a few things like the QL Forum feed, the site search (which is still possible with something like a site-specific Google site: search), but with most if not all of the downloads. Urs König’s copy of my site is available at https://sinclairql.net/djw/index.html

Note that the ‘djw’ part of the URL is actually slightly wrong. The well-intentioned Urs mistook old QL company names! Mine was actually DJC, not DJW, which was a different and unrelated company. But never mind, the copy of the site is available for those who remain loyal to the good old Sinclair QL and the retro community in general.

If I release any more QL software in the future (and that’s definitely an IF, not a WHEN), I’ll probably do so via this blog and anyone will be free to copy anything I release to other more widely accessed QL websites.

Here are a few useful links to the QL pages I used most in the past – I hope they’re all still there.

The Sinclair QL Forum – Index page

Sinclair QL For Everyone | Facebook (QL discussion group on Facebook)

BSJR QL home (Bob Spelten jr.’s QL website)

Q-emuLator Sinclair QL (Daniele Terdina’s Q-emuLator Sinclair QL emulator)

https://sinclairql.net/djw/index.html (copy of my old QL website)

Sinclair QL Home Computer Development · GitHub

Kilgus.net | Stuff I do (Marcel Kilgus website, includes QPC2 emulator)

Knoware (Per Witte’s excellent QL page, many downloads)

Products for Vintage Home Computers (Rich Mellor – RWAP Software)

Buy & Sell Retro Electronics Home Computers Arcade & Video Games Consoles (SellMyRetro site)

https://www.sinclairql.net (Urs König QL site)

GitHub – SinclairQL/sQLux · GitHub (sQLux emulator from Graeme Gregory)

Thierry Godefroy’s Sinclair QL and QDOS compatible systems support site

Quantum Technology – Specialisti Sinclair QL

Monday, 27. July 2026

El porqué de los modos 512×256 y 256×256

19:16 GMT (2 months ago)
Prototipo QL portátil

Imagen: Prototipo inicial del Sinclair QL elaborado por el diseñador Rick Dickinson.

El lanzamiento del Sinclair QL en 1984 introdujo dos modos de vídeo gestionados por su chip a medida (el ZX8301): 512 × 256 píxeles a 4 colores y 256 × 256 píxeles a 8 colores. Desde una perspectiva estrictamente matemática, estas configuraciones arrojaban relaciones de aspecto de 2:1 y 1:1 respectivamente. Sin embargo, las pantallas analógicas y los televisores comerciales de la época empleaban un formato físico de 4:3.

Esta desconexión geométrica no obedeció a un criterio estético, sino a un estricto compromiso de ingeniería para equilibrar el coste de los componentes, los límites de la tecnología de televisión y las necesidades del software profesional.

Como diseñador, siempre he odiado la relación de aspecto del video en el QL. El ordenador, en sus inicios, iba a ser un portátil con pantalla plana. Sabía que Sinclair trabajó duro para crear una pantalla con esta característica, y suponía que otras marcas habrían podido servirle un producto equivalente para el lanzamiento de su nueva máquina. En ambos casos asumí que la resolución de video del QL estaba condicionada por las especificaciones de los fabricantes de las pantallas, pero me puse a indagar con la IA, y descubrí que las causas de su desproporción pudieron ser otras.

La pantalla plana de Sinclair

La portabilidad del Sinclair QL quedó truncada al cancelarse el desarrollo de su monitor integrado. Inicialmente, el proyecto —bajo el nombre en clave ZX83— iba a incorporar una pantalla plana de 4 pulgadas basada en el tubo CRT plano que Sinclair había desarrollado para su televisión de bolsillo TV80, evolución del prototipo FTV2. A diferencia de los LCD de la época, de contraste deficiente, este tubo desviaba el cañón de electrones de forma lateral para reducir drásticamente la profundidad del aparato. Sin embargo, la viabilidad técnica se interpuso en el camino: la compleja electrónica necesaria para corregir la distorsión de la imagen, junto con el elevado coste de fabricación y su modesta resolución, hacían inviable su producción en masa para un ordenador de negocios. Ante esta falta de rentabilidad comercial, Sir Clive Sinclair ordenó cancelar la TV plana a finales de 1983, obligando a rediseñar el QL sobre la marcha como un equipo de sobremesa convencional con salidas de vídeo estándar.

1. El límite económico de los 32 KB de RAM

En la primera mitad de los años 80, la memoria RAM representaba uno de los costes de fabricación más elevados. Para comercializar el QL a un precio competitivo (399 libras en su lanzamiento), los diseñadores de Sinclair Research limitaron el presupuesto de la memoria asignada a vídeo a 32 KB fijos de un total de 128 KB que traía la máquina. Sólo la pantalla ocupaba una cuarta parte de la RAM disponible, aunque, por suerte, el QL permitía expansión de RAM.

Sinclair fijó este tamaño de la pantalla como un compromiso entre coste de memoria y ancho de banda del bus. La resolución de pantalla, además de ser económicamente ajustada, encajaba de forma exacta en la memoria principal sin requerir circuitería adicional de decodificación:

512 x 256 x 2 bits / 8 bits por byte = 32.768 bytes (exactamente 32 KB)

El modo de 256 x 256 comparte exactamente los mismos 32 KB de memoria, ya que el ZX8301 interpreta los mismos bytes de dos formas distintas según el modo que esté en uso.

Lograr una resolución más cercana a la proporción 4:3 con el mismo ancho de 512 píxeles (por ejemplo, 512 × 384), requeriría 48 KB de RAM. Esto hubiera reducido la RAM disponible para el resto del sistema, entre otras cosas, como veremos.

2. El cuello de botella del bus compartido

El QL contaba con un 68008, una versión de 8 bits del 68000, con registros de 32 bits pero bus de datos de 8 bits, y con el chip de video a medida ZX8301. Además de una salida de video compuesto/UHF para televisores domésticos, el QL incorporaba una salida RGB TTL directa para monitores dedicados.

Para ahorrar piezas, se usó un único bloque de memoria compartida para todo el mapa de memoria del sistema, obligando al procesador y al chip de vídeo a utilizar los mismos cables internos para acceder a ella. Si bien esto era común en los ordenadores de la época, otras máquinas usaban trucos de hardware o chips con funciones gráficas dedicadas. Como el chip de vídeo necesitaba leer constantemente esa memoria compartida para evitar que la imagen del televisor se borrase (refresco de pantalla), acaparaba el bus de memoria, imponiendo tiempos de espera al procesador cada vez que este necesitaba leer o escribir en la RAM compartida, reduciendo su ancho de banda efectivo a algo menos de la mitad, y parte de ese tiempo disponible de la CPU se dedicaba a hacer todo el trabajo con la memoria de pantalla (pintado de caracteres, instrucciones gráficas…).

Para intentar aliviar este atasco, Sinclair utilizó una técnica pionera en ordenadores domésticos: hacer que el chip de vídeo leyera la memoria en ráfagas rápidas de varios datos seguidos, ganando un tiempo muy valioso. Sin embargo, este parche tenía un límite marcado por la imagen de la televisión, que se redibujaba 50 veces por segundo (50 Hz), dando al chip de vídeo una ventana de solo 20 milisegundos para procesar cada imagen:

1.000 milisegundos / 50 veces por segundo = 20 milisegundos

Aumentar la resolución del modo de pantalla o añadir más colores por píxel habría obligado al chip de vídeo a pedir más información dentro de ese mismo lapso de tiempo, tardando más, saturando el bus de nuevo e imponiendo aún más tiempos de espera al procesador.

Esta disputa constante por los buses fue el motivo clave para limitar la pantalla a 32 KB. El ZX8301 no lee los datos de golpe, sino que accede a la RAM mediante pequeñas ráfagas continuas a lo largo de los 20 milisegundos de cada fotograma (acaparando 8 de cada 12 ciclos de reloj). Un mapa de pantalla superior a 32 KB habría exigido un ancho de banda mayor, dejando a la CPU sin los pocos ciclos de acceso que le quedaban para trabajar antes del siguiente refresco de imagen que volvería a frenarlo.

Daños colaterales

La búsqueda obsesiva de la velocidad en las ráfagas no solo fijó el tamaño de la pantalla, sino que condicionó la propia codificación de sus píxeles en memoria. En el modo de 4 colores (negro, rojo, verde y blanco), el ZX8301 lee dos planos de bits alternos (R y G) mediante su acceso en ráfaga de 4 bytes, lo que le permite construir 8 píxeles consecutivos con solo dos accesos a memoria. En el modo de 8 colores, empaqueta 4 bits por píxel en palabras de 16 bits, duplicando la profundidad de color dentro del mismo ancho de banda. Un planteamiento brillante para la época pero con importantes consecuencias en el rendimiento gráfico.

Respecto a esa codificación de color, como curiosidad y ejemplo de las limitaciones del hardware, el QL mostraba 8 colores usando 3 bits de color (R, G, B) + 1 bit de parpadeo (FLASH) en vez de 16 colores en el modo de 256 x 256. El ZX8301 generaba señales de vídeo en formato RGB TTL (0 V para nivel bajo o apagado, 5 V para nivel alto o encendido) por cada canal de color. Debido a esta naturaleza binaria, solo podía manejar encendido/apagado (1 bit) por canal. Los monitores dedicados de la época (como el Vision QL) recibían directamente esta señal RGB digital, por lo que estaban limitados a 8 colores base (negro, azul, rojo, magenta, verde cian amarillo y blanco). A cambio, la nitidez y definición de la imagen eran muy superiores a las de un televisor doméstico. El control de brillo en el monitor cambiaba la intensidad luminosa de los colores mostrados, pero no el color.

El parámetro FLASH es un bit que conmuta el estado de cada píxel (haciéndolo parpadear a intervalos regulares de tiempo). Sinclair prefirió el parpadeo por píxel porque el QL se vendía como máquina de negocios, y en interfaces de texto (procesadores de texto, hojas de cálculo) el parpadeo es más útil que un color extra para resaltar campos, cursores o errores.

3. La barrera de los 50 Hz (PAL) y el entrelazado

El QL fue diseñado para poder utilizarse con los televisores domésticos convencionales mediante el estándar de vídeo analógico PAL. El estándar PAL emite a 50 Hz y consta de 625 líneas en total, pero divididas en dos campos entrelazados (líneas pares e impares) de 312,5 líneas cada uno.

Al descontar las líneas ocultas utilizadas para la sincronización vertical de la pantalla, los ordenadores domésticos solo disponían de una ventana útil de entre 256 y 288 líneas para emitir vídeo en modo progresivo (no entrelazado). Fijar la resolución vertical en 256 líneas garantizaba una señal estable y compatible con cualquier televisor.

Superar las 288 líneas verticales para aproximarse a la geometría 4:3 habría obligado al hardware a emitir en modo entrelazado. En los monitores CRT comunes de la época, el texto de alta densidad parpadeaba de forma severa al entrelazarse, provocando fatiga visual inmediata al usuario.

Overscan horizontal

Otra curiosidad. Debido a que las constantes de tiempo del ZX8301 se diseñaron pensando en el tubo CRT plano del prototipo original y no en un televisor convencional, el modo de alta resolución (Modo 4) sufre un claro overscan horizontal (la imagen se sale por los lados). Esto ocurre porque sus 512 píxeles ocupan 51,2 microsegundos de la línea de vídeo, superando la ventana segura de 48 microsegundos que garantiza el estándar PAL. El resultado era que cualquier televisor común recortaba los bordes, perdiendo entre 24 y 32 píxeles de la imagen repartidos en los laterales —algo que solo se veía completo en monitores especiales como el Vision QL—. Un problema que no se solucionó en la fase de diseño.

4. El requisito de las 80 columnas empresariales

El Sinclair QL se posicionó como un ordenador orientado al mercado ejecutivo y de oficina, incluyendo una suite ofimática desarrollada por PSION. Para trabajar con garantías en tareas de procesamiento de textos u hojas de cálculo, el estándar de la industria exigía visualizar un mínimo de 80 columnas de texto.

Al optar por un ancho de 512 píxeles, el QL podía renderizar fuentes tipográficas con una matriz de 6 píxeles de ancho por carácter, logrando acomodar 85 columnas de texto legibles en la pantalla.

Soluciones alternativas en el mercado de 1984

Frente a este mismo dilema técnico, otros fabricantes optaron por caminos distintos según su público objetivo y rango de precio:

  • Apple Macintosh (El camino del monitor dedicado): Lanzado el mismo año que el QL, Apple decidió prescindir por completo de la compatibilidad con televisores. El Macintosh original adoptó una resolución de 512 × 342 píxeles. Para evitar el parpadeo en las 342 líneas, Apple integró un monitor interno de alta frecuencia (22 kHz progresivo en lugar de los 15 kHz de un televisor). Esto permitía píxeles casi cuadrados muy nítidos para oficina, pero disparó el precio de la máquina a 2.495 dólares. Además, al funcionar estrictamente en blanco y negro (1 bit por píxel), el consumo de RAM se contuvo en unos eficientes 21,8 KB.
  • IBM PC con tarjeta CGA (El camino del texto estirado): Para resolver las 80 columnas bajo el límite de los televisores, el estándar CGA de IBM recurrió al modo de 640 × 200 píxeles en blanco y negro (16 KB de RAM). Esto generaba una proporción matemática de 3.2:1, lo que en una pantalla física de 4:3 producía píxeles notablemente más planos y estirados que los del Sinclair QL, deformando de forma severa cualquier elemento gráfico circular o cuadrado.

Píxeles anamórficos

Como consecuencia del diseño, el Sinclair QL operaba con píxeles rectangulares (anamórficos), altos y delgados. Era el propio tubo de rayos catódicos de la pantalla el que se encargaba de estirar físicamente el haz de electrones analógico para cubrir la superficie de cristal 4:3. Los desarrolladores de software debían introducir factores de corrección geométrica en sus rutinas de dibujo para evitar que un círculo perfecto en memoria se mostrase en pantalla con forma elíptica.

Imagen 512x256
Imagen a 4 colores con resolución 512×256 usando píxeles cuadrados antes de ser deformados.

En el modo de 256 x 256, el píxel rectangular se estiraba horizontalmente. La pantalla tenía menos pixels de ancho, exactamente la mitad, pero a cambio ofrecía 8 colores en vez de 4, y cada pixel ocupaba 4 bits en vez de 2 con la misma cantidad de RAM. Como vimos, uno de esos bits se usaba para activar o desactivar el FLASH o parpadeo de píxel, una característica inusual que operaba directamente en hardware sin consumir ciclos de CPU.

El legado de un diseño condicionado

El Sinclair QL permanece como un claro ejemplo de la informática de transición de mediados de los ochenta, donde cada decisión de diseño gráfico estaba sujeta a una estricta realidad física y económica. La elección de las resoluciones de 512 × 256 y 256 × 256 píxeles demuestra que el verdadero reto de Sinclair Research no fue la falta de visión tecnológica, sino la necesidad de equilibrar los costes de producción con los límites técnicos de los hogares de la época. Aunque el uso de píxeles anamórficos obligó a los programadores a realizar encajes matemáticos para evitar deformaciones en las pantallas 4:3, esta arquitectura permitió ofrecer una máquina con capacidades de visualización empresariales a una fracción del coste de sus competidores directos.

Saber más:

Saturday, 25. July 2026

MyLISP para Sinclair QL: cómo acabé escribiendo mi propio LISP (y por qué tiene la culpa una calculadora)

21:44 GMT (2 months ago)

por Daniel Fernandez Santos (@dfernandez132)

Voy a confesar una cosa antes de empezar: de adolescente yo miraba mi calculadora con la misma cara con la que otros miraban el póster de un coche deportivo. Mi HP48 no tenía ruedas ni aceleraba de 0 a 100, pero hacía algo que a mí me parecía mucho más increíble: le escribías 3 X SQ (o cosas parecidas, no me hagáis examinarme de RPL treinta años después) y te devolvía 2·X. No un número. Una expresión. Con una letra dentro. La calculadora sabía derivar sin saber cuánto valía X, y eso, para un chaval con más curiosidad que paciencia, era sencillamente magia negra.

La HP48 —y toda la familia de calculadoras RPL de Hewlett-Packard que la precedieron— tenían una forma de trabajar rarísima si venías de una Casio normal: notación polaca inversa, pila en lugar de línea de edición, y un lenguaje interno, RPL («Reverse Polish Lisp», nada menos), que era en el fondo un LISP disfrazado de calculadora científica. La primera de la saga en atreverse con álgebra simbólica fue la HP-28C, allá por 1987: la primera calculadora de bolsillo capaz de resolver ecuaciones y derivar sin convertirlo todo en decimales. La HP48, que llegó en 1990, heredó ese motor y lo hizo legendario entre estudiantes de ingeniería y gente rara como yo, que la miraba de reojo mientras tecleaba BASIC en mi ZX Spectrum, preguntándome cómo demonios hacía aquello.

Calculadora HP 48SX. Por ella empezó todo

Han pasado bastantes años —los suficientes como para que ya no me sorprenda tanto ver una máquina hacer cuentas, pero sí los bastantes como para que la pregunta se quedara ahí, aparcada, sin resolver—. Y un día, ya mayor y con menos inocencia, pero exactamente las mismas ganas de curiosear, decidí volver a esa pregunta: ¿cómo lo hacía?

La respuesta, como suele pasar, era menos mágica y más bonita de lo que esperaba: dentro de aquella calculadora vivía un CAS, un Computer Algebra System, un sistema de álgebra computacional. Un programa capaz de manipular expresiones matemáticas como símbolos, no como números: sabe que d/dx (x²) = 2x sin necesidad de que le digas cuánto vale x, igual que sabe simplificar, factorizar o sustituir una variable por otra expresión. No calcula el resultado: calcula la fórmula del resultado.

Los CAS no nacieron con la HP48, ni de lejos. La abuela de todos es Macsyma, un proyecto financiado por DARPA (Defense Advanced Research Projects Agency) que arrancó en el MIT (Massachusetts Institute of Technology) allá por 1968 y que durante años sólo pudo correr en mainframes del tamaño de una habitación. A finales de los 70, en Honolulu, dos programadores (Albert Rich y David Stoutemyer, con su empresa Soft Warehouse) consiguieron meter algo parecido en un ordenador de mesa: se llamó muMATH, y de ahí, en 1988, nacería Derive, uno de los primeros CAS que la gente de a pie pudo tener en su propio PC. Ese linaje —Macsyma, muMATH, Derive, Reduce— es el que, con calculadoras de por medio, acabó llegando a mi HP48 sin que yo lo supiera. Hoy en día todo programa serio de cálculo simbólico —Mathematica, Maple, Maxima (descendiente directo de Macsyma y aún vivo), el Symbolic Toolbox de MATLAB, Octave, SageMath— es, en el fondo, un pariente lejano de aquella misma idea.

Bien, misterio resuelto… o casi. Porque en cuanto entiendes qué es un CAS te asalta la siguiente pregunta, mucho más peligrosa: ¿y cómo se programa uno? Ahí descubres que hay dos caminos. Puedes escribirlo en Pascal, en C, en el lenguaje que quieras, tratando las expresiones como si fueran texto o estructuras de datos genéricas, a base de fuerza bruta y mucho if. O puedes hacerlo en el lenguaje que fue literalmente diseñado para esto: LISP (recuerda lo que significaba la última letra del lenguaje de HP RPL), donde una expresión matemática y una expresión de código son, estructuralmente, la misma cosa: una lista. En LISP, escribir un CAS no es forzar la herramienta contra su naturaleza; es usarla para aquello que la inventaron.

Y ahí fue cuando me dije: «así aprendo LISP». Porque LISP tiene una historia tan larga como entretenida. Lo inventó John McCarthy en 1958, en el MIT, y ni siquiera pretendía que se ejecutara en un ordenador de verdad: era una notación matemática para razonar sobre funciones. Alguien lo implementó a mano sobre un IBM 704 (uno de los estudiantes de John), y ahí quedó fijado para siempre uno de los detalles más entrañablemente absurdos de la informática: las funciones CAR y CDR, que hoy en día significan simplemente «primer elemento» y «resto de la lista», vienen literalmente de los nombres de dos registros de aquella máquina: Contents of Address Register y Contents of Decrement Register. Sesenta y pico años después seguimos usando el nombre de un registro de hardware que ya no existe, y a nadie le extraña. Eso es LISP: un lenguaje que arrastra su propia arqueología con orgullo. En los 80 hasta hubo ordenadores enteros diseñados solo para ejecutar LISP —las célebres «Lisp machines» de Symbolics y compañía— antes de que el enfriamiento de la inteligencia artificial se las llevara por delante. Es, después de Fortran, el lenguaje de programación de alto nivel más antiguo que sigue vivo.

IBM serie 700

Y ya puestos a aprender LISP, quise hacerlo en el ordenador de la época de mi HP48 que entonces no tenía, pero miraba con curiosidad, comparándolo con mi ZX Spectrum: el Sinclair QL. El mismo que hoy tengo de verdad y con el que disfruto muchísimo.

Así que me puse manos a la obra: necesitaba un intérprete de LISP para construir mi CAS encima. Miré lo que había disponible… y, ya puestos, hice lo que haría cualquier español de a pie, pronunciar la fatídica frase: “Sujétame el cubata, que esto lo hago yo”.

El resultado se llama MyLISP.

MyLISP es un intérprete LISP-1 (sin entrar en tecnicismos: en un LISP-1 las funciones y las variables comparten el mismo «cajón» de nombres —si defines una variable LIST, adiós función LIST—, mientras que en un LISP-2, como Common Lisp, cada cosa tiene su cajón separado. Es más, una cuestión de filosofía que de complejidad, pero define bastante el carácter del lenguaje). Y desde el primer boceto tuve clara una cosa: no quería un LISP de juguete que sólo sirviera para sumar (1 2 3) en una demo. Quería algo usable de verdad, con closures, listas, recursión seria y una gestión de memoria decente — y eso, en un ordenador de aquella época, exige mínimo 640 KB de RAM para que mi niño respire con comodidad.

Antes de ponerme a teclear, tocaba elegir arma. Lo primero que descarté fue SuperBASIC: es estupendo para casi todo en un QL, pero un intérprete LISP necesita construir sus propias estructuras de datos dinámicas (listas enlazadas, celdas, punteros) con un control fino sobre la memoria, y eso es justo lo que SuperBASIC no te da. Así que volví, con nostalgia, al Pascal de mi juventud: quería recuperar aquella programación imperativa de toda la vida, la de los record y punteros, y el QL me ponía sobre la mesa nada menos que tres compiladores distintos para elegir.

El primero, Computer One Pascal, cayó rápido: compilaba a P-code (una máquina virtual dentro de la máquina), lentísimo para un evaluador recursivo tipo Eval/Apply, y encima tenía un bug de traca que colgaba el sistema en cuanto el QL tenía 1 MB de RAM o más —justo la memoria que yo necesitaba aprovechar. El segundo, Metacomco Pascal, tenía detrás una empresa de las buenas (los mismos que hicieron AmigaDOS y el BCPL para el QL), pero su compilador de Pascal era, según todas las notas históricas de la época, un desastre de bugs y rendimiento; el consejo unánime de la comunidad se resumía en dos palabras: «Forget it!». Así que me quedé con el tercero, Prospero Pascal, el que todo el mundo coincide en señalar como el estándar de facto del QL: certificado ISO 7185, código máquina nativo del 68000 (nada de P-code), coma flotante IEEE de precisión simple y doble hecha a mano, y un enlazador capaz de compilar módulos grandes por separado, algo imprescindible en cuanto tu intérprete Lisp empieza a crecer por encima de las dos mil líneas. Tiene un inconveniente casi entrañable —exige un cartucho EPROM pinchado en el puerto de expansión para funcionar, un dongle antipiratería de otra época—, pero a cambio es el único de los tres capaz de sostener sin temblar un motor de recolección de basura mark & sweep y aritmética exacta. Vamos, que, si iba a jugar en serio, tenía que ser con Prospero.

Anuncio de Prospero Software en QL World Marzo 1986

Tras mil peripecias, unos cuantos bugs de los que sacan canas y algún que otro momento de «por qué me habré metido en esto», conseguí un intérprete que creo que está a la altura de enseñarse a la comunidad sin sonrojo.

Con MyLISP funcionando, tocaba la segunda mitad del plan: el CAS. Ahí tuve que aprender de verdad qué son las funciones lambda, qué son las funciones de orden superior, cómo se manipulan listas cuando las listas son el programa, y qué es eso del «azúcar sintáctico» que tanto mencionan los libros de LISP. Y aquí toca un agradecimiento sincero: Claude ha sido un maestro paciente en todo este tramo, explicándome con calma lo que necesitaba entender de los sistemas de álgebra simbólica hasta que las piezas encajaron.

El resultado es un CAS completo, escrito enteramente en MyLISP, corriendo en mi QL. Y funciona de verdad. Si le doy la expresión 3x² de forma que la entienda:

(DEFINE E (MAKEPROD 3 (MAKEPOW 'X 2)))

Y le digo que me la derive respecto a x:

(DERIVA E 'X)

Me devuelve:

> (* 6 X)

Pero si además le digo que quiero que me lo ponga en notación matemática normal:

(PRINTMAT (DERIVA E 'X))

Me muestra en pantalla:

> 6x

Ese 6x es el CAS imprimiendo la derivada en, después de haberla calculado símbolo a símbolo.

Y aun podemos hacer más cosas con este CAS para LISP (observad que he dicho para LISP no para QL…. eso en el próximo articulo) no se queda en derivadas de una variable: también hace derivadas parciales cruzadas, sustituciones, simplificación de árboles de expresiones, orden canónico de términos semejantes… el motor completo que treinta años atrás yo miraba con ojos como platos en la pantallita de mi HP48, ahora vive, hecho a mano, en el ordenador que de niño ni siquiera tenía.

Se cierra el círculo. Con más canas, pero con las mismas ganas.

Podéis acceder a MyLisp en https://github.com/dfernande132/MyLISP

Podéis acceder al CAS en https://github.com/dfernande132/MyLISP-CAS

Prospero Pascal está disponible en la página de Dilwyn: https://sinclairql.net/djw/language/index.html

Daniel Fernandez Santos , Julio 2026

Saturday, 09. May 2026

Crónicas RU Sinclair QL y Compatibles 2026

18:42 GMT (4 months ago)

Por Miguel Ángel Rojo

El día 1 de mayo, en Majadas de Tiétar comenzó con la llegada de los asistentes al evento RU Sinclair QL y Compatibles 2026 el viernes: Badaman, Zerover, Naspternds, TitoxUnix, DFSantos, Salvador Merino, Álvaro Alea y un servidor, Miguel Ángel Rojo. Desempaquetamos todas las máquinas que habíamos traído, dejándolas preparadas para la mañana siguiente, cuando comenzaría el esperado cacharreo. Así pudimos empezar a ver todo lo que los compañeros habían llevado al encuentro. La experiencia fue increíble. Mesa por mesa, tanto el hardware clásico como los desarrollos modernos, exponían la historia completa del Sinclair QL, desde sus inicios hasta la actualidad, demostrando que sigue más vivo que nunca.

Lo primero que llamó nuestra atención fue la impresionante mesa que Naspternds había preparado, presidida por varios Sinclair QL acompañados de sus monitores. Entre todas las piezas destacaban una codiciada Aurora y un espectacular CST Thor, adquirido recientemente y equipado con una unidad Gotek.

Durante la jornada se realizó el esperado unboxing de una nueva tarjeta QLion Gold recién llegada de Grecia. El dispositivo demostró ser una maravilla, nos permitimos disfrutar de títulos como Old Tower, acompañado de una excelente calidad de sonido gracias al QLSound, así como del magnífico Batman remake de Joan Gayón.

Otro de los grandes atractivos fue la presencia de una interfaz QL MIDI de Miracle Systems, una pieza extremadamente rara de la que apenas existen unidades en circulación. Gracias a este hardware pudimos reproducir archivos MIDI a través de un equipo japonés especializado, ofreciendo una experiencia sonora excepcional y convirtiéndose en uno de los momentos más memorables del encuentro.

La activa implicación con la plataforma por la clonación y desarrollo de hardware para QL de TitoUnix, le convierten en uno de los miembros más participativos de la escena. Entre sus proyectos más destacados se encuentra la QubIDE clónica, además de diversas ampliaciones de memoria para el sistema, el ensamblaje de la nueva placa base de QL completamente funcional, especialmente teniendo en cuenta la dificultad de localizar determinados componentes originales, aunque todavía es posible encontrar algunos de ellos.

TitoxUnix continúa desarrollando nuevas ideas y proyectos para la plataforma, demostrando que la escena del Sinclair QL sigue muy viva gracias al entusiasmo y dedicación de desarrolladores y aficionados como él, también pudimos ver su impresionante colección de Qls.

Zerover también nos presentó su trabajo en el desarrollo de un teclado para Sinclair QL, además de una gran cantidad de PCB diseñadas por él mismo mediante KiCad.

Su mesa despertó un enorme interés entre los asistentes, no solo por la calidad de sus diseños, sino también por la dedicación y el esfuerzo invertidos en cada uno de los proyectos mostrados.

Otro de los momentos destacados fue el intento de recuperar un microdrive de PSION adquiridos recientemente por un nuevo aficionado al mundo del QL. Se trata de una situación con la que muchos de los que hemos llegado más tarde a esta plataforma nos hemos encontrado: microdrives averiados, cintas deterioradas o membranas quebradas forman parte de la experiencia habitual de restauración y conservación de estos equipos históricos. Durante las pruebas se intentó realizar una copia de seguridad del software, sin embargo, al tratarse de programas de PSION protegidos y en versión española, todo apunta a que únicamente funcionarían correctamente en un QL español, presentando incompatibilidades con sistemas equipados con la ROM inglesa.

Álvaro Alea también nos deleitó con una impresionante colección de placas y desarrollos fabricados por él mismo, demostrando un enorme conocimiento técnico y una gran dedicación al ecosistema del Sinclair QL.

Entre sus proyectos, destacaban varias expansiones RAM -que también están publicadas en su GitHub-, diseñadas para ampliar la memoria del sistema y ofrecer nuevas posibilidades a los usuarios del QL. También pudimos ver clones de la QL Trump Card, una de las expansiones más reconocidas dentro de la plataforma por sus capacidades de memoria y gestión de discos. Entre sus diseños en GitHub se pueden ver cantidad de proyectos: Interfaz de SINCLAIR QL Mini Trump Card, QL 4 floppy addon, Minerva MK2, un clon de la conocida placa ROM pensado para aportar mayor funcionalidad y estabilidad al equipo…

Otro de los desarrollos que despierta gran interés en el mundillo QL es la tarjeta de sonido QLsound, capaz de mejorar notablemente las capacidades de audio del sistema original. Todos estos proyectos reflejan el excelente momento creativo que sigue viviendo la escena del Sinclair QL, donde la comunidad continúa desarrollando nuevo hardware y manteniendo viva una plataforma con más de cuatro décadas de historia a sus espaldas.

Badaman nos trajo varios QLs en los que se estuvieron haciendo varias reparaciones. En uno de ellos se disponía un pieza muy útil, un multirom, basada en la ROM estándar 27C512 instalada en un zócalo. Lleva un conmutador externo con una memoria ROM de 512 KB, lo que permite tener 8 imágenes ROM. El selector de ROM tiene un interruptor de 4 posiciones, 3 de ellas para seleccionar la imagen de la ROM, y el último para activar o no el Toolkit 2. Esta selección se puede hacer sin abrir la máquina, desde la parte de atŕas del QL. Y como almacenamiento interno un QubIDE con ampliación de memoria de José Leandro. Otro equipo para reparar era un QLUB, un aparato para conectar por los puertos de red de QL a un PC con un emulador QL. Badaman es el artífice de la recuperación de material del Club Qlave que recopiló Salvador Merino, posteriormente publicado en la web Sinclair QL Recursos en Castellano, y quien dio origen al grupo de amigos que participamos en este evento.

DFSantos nos deleitó con su QL y un  Spectrum Next, para el cual existe un core de QL muy logrado que pudimos ver allí. Nos presentó su libro “Inteligencia Artificial para el Z80: Si puedes construirlo en 8 bits, realmente lo entiendes” una nueva obra que propone mirar al pasado para entender el futuro. Escrito por José Daniel Fernández Santos, el libro plantea una idea tan simple como fascinante: aprender inteligencia artificial implementando sus conceptos directamente sobre un procesador Z80, el mismo corazón que latía dentro de máquinas tan nostálgicas como el ZX Spectrum, MSX o Amstrad CPC.

La participación de Salvador Merino fue de los momentos destacados del encuentro. Salvador representa la trayectoria dentro de la escena QL española y su papel al frente del Club Español Independiente de Usuarios de Sinclair QL (CEIUQL), conocido como QLave, que mantuvo activo desde Málaga entre 1987 y 1997. El club ayudó a numerosos usuarios en una época en la que conseguir información y software para el QL era especialmente difícil en España. Salvador también compartió sus experiencias en el mundo QL, reafirmando la importancia histórica y técnica que el Sinclair QL sigue teniendo para su comunidad de usuarios. En esta ocasión llevó al evento su caja y teclado profesional SPEM, un kit italiano que se vendía para hacer que el QL fuera más cómodo de usar.

La mesa de Miguel Angel Rojo tenia un completo Sinclair QL equipado con una expansión TDI, ampliación de memoria, controladora Trump. Como sistema de almacenamiento masivo una QIMSI, en la que se utilizan archivos con extensión .WIN, también conserva una unidad de microdrive como mdv2_ y otra unidad MicroPicoDrive que la utiliza como mdv1_, un elemento cargado de nostalgia que sigue siendo imprescindible para los aficionados que nos gusta usar este medio de almacenamiento.

Durante la jornada se realizaron diversas pruebas utilizando QLUB conectado a un portátil que ejecutaba el emulador QemuLator. Gracias al puerto QLnet fue posible transferir archivos entre ambos sistemas mediante un sencillo cable de audio mono, demostrando una vez más la versatilidad y actualidad del ecosistema QL.

Entre otros, también estuvo presente nuestro anfitrión, Carlos Izquierdo, que nos hizo de guía con su gran amabilidad al tremendo recinto del Museo y su impresionante colección de ordenadores de casi todas las décadas. Con él hicimos un viaje por la historia de la computación a través de más de 800 computadoras con las que cuenta la exposición. Seguramente la mayor colección de ordenadores de toda España. Algunos ordenadores fueron encendidos en esa visita. Pudimos ver computadoras de gran escala como supercomputadoras y mainframes, pasando por minicomputadoras, microcomputadoras, PCs, estaciones de trabajo y finalmente una sección con más de 100 videoconsolas.

Se dejaron caer algunos asistentes, que fueron bienvenidos y pudieron experimentar de cerca lo que es un QL en su máximo esplendor. También estuvieron presentes los amigos del museo que nos acompañan en cada evento.

En definitiva, la experiencia fue extraordinariamente positiva y nos deja con el deseo de repetir el próximo año. Una vez más, quedó patente que los entusiastas de la Sinclair QL continuamos manteniendo viva la pasión por esta máquina, demostrando día a día nuestra fidelidad e implicación, pese a las dificultades y al desafortunado devenir que marcó parte de su historia.

Vídeos y fotos del evento

  • Visita la galería de fotos aquí.
  • Ver el video realizado por el Museo de Historia de la Computación aquí.
  • Ver el video elaborado por napsternds aquí.

Monday, 04. May 2026

ZX2SB: Cambio de idea de los temas especiales

08:49 GMT (4 months ago)

Índice de entradas del conversor

Modificado el 04/05/2026, cambios en rojo 


Casos especiales: GO TO / GO SUB y variables con espacios

Había decidido tratar estos casos en el parser por comodidad, pero al final hacer las cosas de forma correcta es lo adecuado. En lugar de introducir excepciones en el parser, es mejor que el lexer los trate correctamente desde el principio y devuelva ya los tokens adecuados.

GO TO y GO SUB

Además de poder escribirse en una o dos palabras, he detectado que en un Spectrum +2/+3 (ya que en el «gomas» todo va por tokens y no es posible escribir otra cosa) se puede introducir GOTO20 y el sistema lo separa correctamente como GO TO 20.

Ante esto, he modificado el lexer para que, cuando reciba una sentencia de salto, la trate correctamente en todas sus variantes.

De esta forma, las sentencias GO TO 20, GOTO 20, GO TO20 o GOTO20 generan siempre los mismos dos tokens:

  • TK_GOTO
  • TK_NUMERO 20

Y lo mismo ocurre con GOSUB, generando correctamente TK_GOSUB y TK_NUMERO.

Nombres de variables con espacios

Realmente no he visto ningún programa que utilice nombres de variables con espacios. Por tanto, he optado por simplificar el caso: si se detectan espacios en un nombre de variable, se eliminan directamente, generando un nombre compacto.

Así, cuando recibo:

LET ANTES O DESPUES = 4

El lexer genera directamente:

  • TK_LET
  • TK_VARIABLE antesodespues

Procesado en dos fases

Para realizar este cambio, tras buscar distintas alternativas durante la lectura del código y comprobar que el manejo directo se complicaba innecesariamente —especialmente por la coexistencia de palabras reservadas que no admiten espacios y nombres de variables que sí pueden contenerlos—, opté por una solución en dos fases.

El caso de variables con espacios será poco habitual, pero existe, y preferí resolverlo de forma explícita en lugar de cargar al parser con lógica adicional para anticipar combinaciones poco frecuentes. 

 En la primera fase se realiza únicamente el proceso de tokenización. Durante esta etapa, si se detecta que existen nombres de variables separados por espacios, que generan varios tokens consecutivos, al finalizar el proceso del Lexer se pregunta al usuario si desea procesarlos.

En la segunda fase, se reconstruyen esos nombres de variable, uniendo los tokens que originalmente estaban separados por espacios en un único identificador sin ellos, verificando previamente que el nombre resultante no coincida con ninguna palabra reservada del lenguaje.

Por ejemplo, ante una secuencia de tokens como:


TK_LET
TK_VARIABLE antes
TK_VARIABLE o
TK_VARIABLE despues

Se genera:


TK_LET
TK_VARIABLE antesodespues

Ficheros intermedios

Este proceso genera dos ficheros intermedios:

  • .TOK, que contiene la tokenización inicial
  • .TOS, que contiene los identificadores ya normalizados

De este modo, el parser puede elegir directamente cuál de los dos ficheros debe procesar, sin necesidad de realizar comprobaciones adicionales ni de modificar el mismo fichero en varias pasadas.

Separación de responsabilidades

Esta decisión refuerza una separación clara de responsabilidades dentro del diseño del sistema:

  • El lexer se encarga exclusivamente de analizar el texto y generar tokens.
  • La normalización de identificadores se realiza como una fase independiente, consciente y explícita.
  • El parser puede centrarse únicamente en la estructura sintáctica, sin necesidad de anticipar ni corregir decisiones tomadas en fases anteriores.

Evitar que el parser tenga que buscar patrones por adelantado o reinterpretar secuencias ambiguas simplifica enormemente su implementación y lo hace más robusto y mantenible.

Pensando en el transpilador en SuperBASIC

Esta estrategia resulta especialmente útil de cara al futuro transpilador desarrollado en SuperBASIC, que no destaca precisamente por su facilidad para manejar ficheros. Generar dos ficheros independientes, en lugar de leer y modificar uno sobre la marcha, simplifica mucho la implementación y reduce el riesgo de errores.

En definitiva, se trata de priorizar una solución sencilla, explícita y robusta: cuanta menos lógica compleja se introduzca en las fases críticas del proceso, más fácil será mantener y evolucionar el sistema a largo plazo.

Friday, 01. May 2026

ZX2SB: Mejorando el sistema de evaluación de expresiones y cambios menores

10:26 GMT (5 months ago)

Índice de entradas del conversor

Cambios el 01/05/26 marcados en rojo 


Mejorando el transpilador: IR estructurado y parser avanzado

En el desarrollo de un compilador no solo importa que el código “funcione”, sino que cada fase esté bien delimitada y tenga responsabilidades claras. En este artículo repasamos una mejora importante en el proyecto actual del transpilador de ZX BASIC: una revisión de la arquitectura general, la resolución de algunos retos léxicos concretos del lenguaje y, sobre todo, un cambio estructural en el parser que impacta directamente en el análisis semántico y en la generación de código.


Índice


1. Arquitectura del compilador: lexer, parser, semántico y generador

Los compiladores suelen dividirse en cuatro fases clásicas, cada una con un papel bien definido. Esta separación es fundamental para mantener el código extensible, depurable y preparado para futuros backends.

Lexer (analizador léxico)

El lexer es la primera fase del proceso. Su tarea es transformar el texto fuente en una secuencia de tokens: identificadores, números, palabras clave, operadores y signos de puntuación. En esta fase se ignoran detalles como espacios irrelevantes o comentarios y se normaliza el formato.

Una decisión clave del diseño es que el lexer produzca tokens semánticamente completos. Esto evita que el parser tenga que reinterpretar combinaciones de palabras o resolver ambigüedades que no le corresponden.

El lexer solo conoce los elementos básicos del lenguaje: separadores, delimitadores y palabras reservadas. Un tema importante que se resuelve aquí es el de los comentarios, que pueden variar desde el simple REM del BASIC clásico hasta los de estilo C de una línea // o de várias líneas /* */.

Habitualmente los comentarios se eliminan en esta fase, pero en nuestro caso se conservan ya que el objetivo es un transpilador en el que puedan mantenerse o descartarse a elección.

Parser (analizador sintáctico)

El parser recibe la secuencia de tokens y la interpreta según la gramática del lenguaje. El parser trabaja por sentencias completas de una o varias líneas, lo que en nuestro caso se simplifica ya que el BASIC se estructura por líneas, por lo que nuestro parser trabaja línea por línea, detectando sentencias, expresiones y separadores como : o el fin de línea.

El resultado del parser debería ser un árbol sintáctico, pero he optado por una forma simplificada: una representación intermedia (IR) estructurada, que conserva la forma del programa pero es más fácil de analizar y transformar.

Inicialmente el parser emitía sentencias “ajustadas” como texto, lo que obligaba al semántico y al generador a volver a analizarlas. Esto se ha corregido parcialemnte emitiendo ya tokens y estructuras explícitas desde el parser.

El parser detecta errores estructurales (por ejemplo, un LET sin un =), pero no valida aún el significado profundo del programa.

Semántico

El análisis semántico valida el significado del programa: existencia de variables, tipos, uso correcto de arrays, coherencia de expresiones y saltos válidos. Aquí ya no importa cómo estaba escrito el código original, sino qué representa.

Esta fase se beneficia enormemente de que el parser proporcione estructuras claras y no cadenas ambiguas.

Generador de código

Por último, el generador traduce la IR validada a un backend concreto, como SuperBASIC o ensamblador 68000. Al trabajar sobre estructuras bien definidas, puede centrarse únicamente en cómo emitir el código.


2. Resolviendo casos especiales: GO TO / GO SUB y variables con espacios

He cambiado ligeramente esto, ver la entrada Cambio de idea en los temas especiales, la idea es la misma solo cambia la resolución.

GO TO y GO SUB

El ZX BASIC tiene particularidades históricas heredadas del ZX80/81, máquinas con 1Kb de memoria, por lo que ahorrar espacio era fundamental. En el Spectrum un tema llamativos es el uso de sentencias formadas por dos palabras:

  • GO TO en lugar de GOTO
  • GO SUB en lugar de GOSUB

Esto se produce porque heradado de los ZX80/81, internamente el Spectrum no almacena cadenas de texto para las palabras reservadas, sino códigos dentro del propio juego de caracteres. Por ejemplo, al ejecutar PRINT CHR$(236) aparece en pantalla GO TO, lo que hace que internamente sea un solo código, pero se presente en pantalla como 5 caracteres.

Aunque en los listados pueda verse separado, semánticamente siempre es una única sentencia. Para tratarlo correctamente y aceptar las dos formas de escribir la sentencia, el lexer emite tokens separados para TK_GOTO, TK_GOSUB que ya se tratan sin problemas, y además para TK_GO, TK_TO, TK_SUB, y he optado por resolver este segundo caso en el parser por comodidad:

  • Si aparecen TK_GOTO o TK_GOSUB, se aceptan directamente
  • Si aparece TK_GO, se mira el token siguiente
    • Si es TK_TO, se genera un único token TK_GOTO uniendo ambos tokens.
    • Si es TK_SUB, se genera TK_GOSUB uniendo ambos tokens.
    • Cualquier otro caso es un error.

Así, el resto del parser y el semántico trabajan siempre con sentencias completas sin ambiguedades, y aunque parezca un poco inconsistente usar el mismo token para dos cosas, de esta manera tampoco hay conflictos con el TO usando en el bucle FOR.

Nombres de variables con espacios

Otra particularidad del ZX Spectrum es que los nombres de las variables pueden contener espacios, los cuales son ignorados por el intérprete. Así, A B es equivalente a AB. Este comportamiento proviene del modo en que el analizador de sentencias elimina los espacios considerados no significativos durante el análisis. No obstante, en mi opinión esto no responde a una decisión de diseño consciente, sino más bien a un bug del intérprete que no contempla correctamente este caso.

No he encontrado documentación oficial que lo acredite explícitamente como un bug, aunque no soy el único que lo interpreta así, como puede verse en este comentario técnico: los nombres de variables pueden contener espacios .

Como curiosidad histórica, en el lenguaje FORTRAN los espacios tampoco son significativos en ningún punto de la sentencia. Esto daba lugar a ambigüedades bien conocidas, como el clásico ejemplo:

    DO10I=1,10

donde el compilador no puede distinguir, hasta encontrar la coma, si se trata de una asignación:

    DO10I = 1

o de un bucle:

    DO 10 I = 1,10

En ZX BASIC este comportamiento permite construcciones como esta que funcionan correctamente y muestran un 4 en pantalla:

    LET A B=4 : PRINT AB

Para evitar estas ambigüedades en el transpilador, el lexer separa por espacios y produce varios identificadores en este caso, uno para A y otro para B, luego en el parser se detecta que existen varios  identificadores consecutivos y se unen en uno solo, sustituyendo los espacios por un guion bajo (_) y generando la variable A_B. Como este carácter no pertenece al juego de caracteres del Spectrum, no puede entrar en conflicto con el código original. De este modo, el parser emite internamente:

LET A_B=4 : PRINT AB

El semántico considera equivalentes A_B y AB, y finalmente el generador emite el nombre correcto para el backend, produciendo:

    LET AB=4 : PRINT AB

sin ambigüedades y totalmente compatible con SuperBASIC.


3. IR estructurado, PRINT dividido y polaco inverso

No me resultaba satisfactorio que el parser enviara información que luego el semántico debía volver a analizar. Por ello he cambiado la representación de expresiones y PRINT, acercándola más a un árbol real, pero usando una forma lineal conocida como montón (heap), que corresponde al recorrido en postorden del árbol.

División estructurada del PRINT

En versiones anteriores del transpilador, el PRINT se generaba como texto concatenado, lo que obligaba al semántico y al generador a reinterpretar de nuevo su contenido. Esto resultaba frágil y poco extensible. Para evitarlo, ahora el parser divide cada PRINT complejo en una secuencia de elementos estructurados.

Cada elemento del PRINT se representa como una unidad independiente que contiene:

  • El tipo del elemento (cadena, variable, AT, TAB, INK, etc.)
  • El separador asociado (;, , o ninguno)
  • El valor completo del elemento

Por ejemplo, la siguiente línea en ZX BASIC:

    PRINT AT 3,4,"Nombre: ";N$;TAB(20);INK 3;"Edad: ";EDAD

Es descompuesta por el parser en una serie de instrucciones PRINT independientes, cada una representando un único elemento lógico:

    PRINT <AT>        <3,4>        <,>
    PRINT <CADENA>    <"Nombre: "> <;>
    PRINT <VARIABLE>  <N$>         <;>
    PRINT <TAB>       <(20)>       <;>
    PRINT <INK>       <3>          <;>
    PRINT <CADENA>    <"Edad: ">   <;>
    PRINT <VARIABLE>  <EDAD>       <none>

Esta representación intermedia permite que cada fase posterior trate los elementos de forma adecuada, sin necesidad de volver a analizar texto.Durante la generación de código para SuperBASIC, estas instrucciones se traducen en:

    AT 3,4            
    PRINT ,           
    PRINT N$;
    PRINT TO 20;      
    INK 3             
    PRINT "Edad: ";
    PRINT EDAD

Hay varios cambios en SuperBASIC que obligan a hacerlo de esta manera:

  • AT ya no forma parte del PRINT, se debe lanzar por separado
  • Los modificadores gráficos (INK, PAPER, etc.) tampoco se pueden usar ya dentro de un PRINT 
  • TAB cambia el nombre por TO, y no se deben usar paréntesis en su parámetro (en el Spectrum se puede usar con o sin paréntesis) 
  • Los separadores se tratan siempre correctamente. La segunda línea generada parece estraña, pero reemplaza a la coma tras el AT en la sentencia original, que ya no forma parte del PRINT. Pero la coma es necesario incluirla para mantener lo que intenta presentar en pantalla el programa original. Como usar una coma tras un AT o tras un TAB no tiene sentido, el parser emite un warning de aviso únicamente.

El resultado es un código más limpio, correcto y fácil de generar, sin alterar para nada el código original, solo adaptado a la forma de trabajar del SuperBASIC.

Expresiones en polaco inverso (RPN), árbol y montón

Hasta ahora, el parser analizaba las expresiones, pero luego las enviaba prácticamente en bruto al semántico, que debía volver a analizarlas. Esto implicaba repetir trabajo y hacía el sistema más frágil. Para evitarlo, el parser genera ahora una representación estructurada de las expresiones basada en el recorrido del árbol sintáctico.

Internamente, toda expresión puede representarse como un árbol, donde los nodos son operadores y las hojas son operandos (variables, literales, constantes, etc.). Por ejemplo, la expresión:

A + B * C

se puede representar mediante el siguiente árbol:

    +
   / \
  A   *
     / \
    B   C

En lugar de almacenar explícitamente el árbol, el parser lo recorre en postorden (primero los hijos, luego el nodo) y guarda el resultado en una estructura lineal denominada montón (heap). Esta representación corresponde a lo que se conoce como notación polaca inversa o RPN (Reverse Polish Notation):

A B C * +

El montón es, por tanto, un árbol almacenado de forma lineal: conserva toda la información estructural, pero sin necesidad de punteros ni referencias explícitas, lo que facilita enormemente su almacenamiento y recorrido.

La evaluación de una expresión en RPN se realiza mediante un autómata de pila, siguiendo una regla muy simple: cuando se lee un operando se apila, y cuando se lee un operador se desapilan los dos elementos superiores, se aplica la operación y se vuelve a apilar el resultado.

Siguiendo el ejemplo anterior, la evaluación de A B C * + se realiza así:

Lee A  -> se apila A                (A)
Lee B  -> se apila B                (B, A)
Lee C  -> se apila C                (C, B, A)
Lee *  -> C * B, se apila resultado (C*B, A)
Lee +  -> (C*B) + A                 (resultado final)

La gran ventaja de este enfoque es que ya no son necesarios paréntesis ni reglas implícitas de precedencia durante la evaluación: el orden de las operaciones está completamente determinado por la posición de los elementos en el montón. La única responsabilidad del parser es generar correctamente el árbol y recorrerlo respetando la prioridad de los operadores.

Gracias a esta representación, el semántico puede validar fácilmente las expresiones (tipos, operaciones permitidas, número de operandos), y el generador puede emitir código eficiente sin necesidad de reinterpretar la expresión original.

Impacto en el semántico y el generador

  • El semántico trabaja directamente con estructuras, sin reinterpretar texto.
  • El generador emite código más limpio y eficiente.

Este enfoque hace que el transpilador sea más robusto, más extensible y más preparado para múltiples backends.


Este tipo de decisiones estructurales, aunque no siempre visibles desde fuera, son las que marcan la diferencia entre un traductor funcional y un compilador sólido y mantenible (y sí, también sirven de consuelo cuando uno se da cuenta de que ha tenido que volver a  rehacer parte del camino por no preveerlo desde el inicio).


ZB2SB. Conversor de Basic de los ZX al Super Basic del QL. Indice

10:21 GMT (5 months ago)

Segundo intento de desarrollo, a ver si esta vez se completa

Este proceso está muy desordenado, ya que he ido alante y atrás todo el rato cambiando el planteamiento de las cosas varias veces, además de ir añadiendo mejoras y simplificando temas, pero es lo que hay cuando no haces un estudio completo antes de empezar, ir programando sobre la marcha tiene estos temas.


01. ZB2SB. Conversor de Basic de los ZX al Super Basic del QL (Reinicio)
02. Definir el Lenguaje de origen
03. DIRECTOR DEL PROCESO
04. Estructuras de datos
05. LEXER. ANALIZADOR LÉXICO. Estructuras de datos para el Lexer
06. LEXER. ANALIZADOR LÉXICO. Planteamiento
07. Un nuevo comienzo
08. PARSER. Analizador semántico
09. GENERADOR de código en SuperBASIC 
10. Otra vuelta atrás (y van...)
11. Mas mejoras (los cambios no acaban nunca)
12. Cambio de idea en los temas especiales

 


Primer intento de desarrollo, fallido, pero quizá te interese ver alguna cosa

01. Introducción
02. Analizador Léxico primera parte: Lectura de líneas 
03. Analizador Léxico segunda parte: Extractor de Tokens

Tuesday, 28. April 2026

ZX2SB: Otra vuelta atrás

09:53 GMT (5 months ago)
Rediseño del IR en ZX2SB: por qué abandonar el AST y conservar tokens semánticos

Índice de entradas del conversor


Otra vuelta atrás: IR con tokens

Como ha ocurrido otras veces durante el desarrollo del proyecto, cada vez que empiezo a generar código válido aparece un problema no contemplado y me obliga a retroceder un paso más. En este caso ha sido un tema de espacios excesivos.

No sirve de nada generar código como:

LET A ( C + D ) = G ( 3 * 4 )

en lugar de:

LET A(C+D)=G(3*4)

En una máquina con recursos limitados, cada carácter cuenta. Podría parecer sencillo “eliminar espacios”, pero eso llevaría a errores como convertir:

IF A AND B THEN C = 3

en algo no válido como:

IF AANDB THEN C=3

Para usar únicamente los espacios imprescindibles es necesario identificar correctamente palabras reservadas, variables, operadores y separadores. Eso implica que los tokens deben estar disponibles en la fase correcta: el generador. Por simplificar el IR inicial, no los incluí… y ahora solo quedaban dos opciones: volver a tokenizar en el generador (lo cual es absurdo) o generar el IR conservando los tokens.

En esta entrada documento otro de los cambios arquitectónicos realizados en ZX2SB. Tras decidir no usar un AST completo por su complejidad, pasé a un IR lineal simple. Ahora el paso lógico es evolucionar hacia un IR lineal basado en tokens semánticos.

Aunque puede parecer otra “vuelta atrás”, en realidad es una decisión motivada por la naturaleza del lenguaje BASIC, por la experiencia práctica adquirida durante el desarrollo del transpilador y, sobre todo, por la necesidad de generar código SuperBASIC limpio, editable y sin reconstrucciones artificiales.


Índice


¿Qué es el IR en ZX2SB?

Nota auxiliar: Un AST (Abstract Syntax Tree) es una estructura de datos en forma de árbol que representa la organización sintáctica de un programa tras el análisis del parser, eliminando elementos puramente textuales y conservando únicamente su estructura semántica. Cada nodo representa una construcción del lenguaje y se almacena normalmente en memoria como una jerarquía de objetos, aunque también puede serializarse para depuración u otros pasos intermedios del compilador.

En lenguajes más complejos y de estructura libre como C o Pascal, el AST es imprescindible. BASIC, en cambio, es un lenguaje más sencillo y organizado por líneas, lo que permite simplificar su representación interna.

En ZX2SB, el IR (Intermediate Representation) es la forma intermedia generada tras el análisis sintáctico y semántico del ZX BASIC original, y sirve como base para:

  • Renumeración de líneas
  • Reescritura de saltos
  • Optimización estructural
  • Generación del código SuperBASIC final

El IR no es solo texto, pero tampoco es ejecutable: describe la intención del programa sin la complejidad de manejar un árbol completo, y además se puede guardar fácilmente como texto lineal.


El problema del AST completo en BASIC

Un AST clásico es ideal para lenguajes donde la semántica está profundamente anidada y la estructura es jerárquica.

BASIC, y en particular ZX BASIC, tiene características distintas:

  • Sentencias lineales
  • Control de flujo basado en números de línea
  • Expresiones relativamente simples
  • Poca recursividad sintáctica

Mantener un AST completo obligaba a aplanar el árbol para generar código, volver a decidir espaciado y operadores, y en muchos casos reinferir información que ya se conocía.

“¿Por qué tengo que volver a tokenizar o reinterpretar lo que yo mismo ya había parseado?”

Durante un tiempo decidí continuar con el IR simple porque era “suficiente”, pero al final la realidad es que los errores no se mantienen: se refactorizan.


La decisión: IR lineal con tokens semánticos

La solución adoptada es intermedia entre un AST completo y un IR puramente textual:

Un IR lineal, basado en tokens tipados y semánticamente anotados.

Cada sentencia BASIC se representa mediante:

  • Un tipo de sentencia (IF, LET, FOR, PRINT…)
  • Campos estructurales explícitos
  • Expresiones como listas ordenadas de tokens

No existen árboles internos complejos ni se guardan nombres textuales de sentencias, sino identificadores internos, lo que simplifica enormemente el trabajo posterior.


Tokens canónicos y eliminación de ambigüedad

Uno de los problemas recurrentes era usar nombres distintos para el mismo token en distintos módulos. Un simple Par_Ape frente a ParenIzq puede provocar horas de depuración absurda.

La solución fue eliminar completamente:

  • Nombres textuales de tokens
  • Strings mágicos entre módulos
  • Dependencias implícitas entre fases

En su lugar se introdujeron TokenID canónicos:

  • Identificadores numéricos estables
  • Definidos en un único enum
  • Usados por lexer, parser, IR y generador

Ahora todo el sistema habla exactamente el mismo idioma.


Espacios, formato y generación correcta

El nuevo IR resolvió definitivamente el problema del espaciado. La idea clave es simple:

“No eliminamos espacios. Decidimos cuándo ponerlos.”

Al conservar operadores, paréntesis y separadores como tokens reales, el generador puede emitir código compacto o legible, sin ambigüedades y sin depender de parches, y evitando usar expresiones regulares, no se soportan en SuperBASIC y el objetivo final es portar el código de ZX2SB a SuperBASIC.


Ventajas prácticas del nuevo IR

  • No se recompone información ya conocida
  • No se retokeniza texto generado
  • El IR es imprimible y depurable
  • El código SuperBASIC generado es editable
  • El sistema escala sin hackear fases anteriores
“El diseño deja de luchar contra el lenguaje y empieza a trabajar con él.”

Frase muy bonita que solo quiere decir que me auto animo a seguir adelante, a pesar de las peleas que tengo contínuamente con el código, es mi primer "compilado" completo y se nota.


Conclusión

Abandonar un AST completo no significa perder rigor si el IR está bien diseñado. En ZX2SB ha supuesto mayor coherencia, menos fases ficticias y un generador más simple y fiable.

Como suele ocurrir en ingeniería de lenguajes, la solución elegante no fue añadir más estructura, sino conservar solo la estructura que importa.


ZX2SB Project (algún día será un proyecto serio; de momento es una fuente constante de problemas… y un entretenimiento mental bastante divertido)

Thursday, 23. April 2026

Pistorm para Sinclair QL

20:28 GMT (5 months ago)

El usuario de QL Forum Will James lleva varios meses trabajando en una versión de Pistorm para Sinclair QL. Hoy ha hecho el anuncio de que ha completado el desarrollo:

https://www.theqlforum.com/viewtopic.php?p=71508#p71508

Estará disponible en tres versiones, de las que hará públicos los archivos para que cualquiera pueda montársela:

1) Expansion port Pi Zero 2
2) Expansion port CM4
3) Internal CM4

Según los bechmarks que ha publicado, ha conseguido alcanzar un redimiento de 40x el QL estándar.

Estaremos atentos a la disponibilidad para poder probarla y comentar nuestras impresiones.

Una gran noticia para el QL!!!

Monday, 20. April 2026

ZX2SB: El generador de código SuperBASIC

11:52 GMT (5 months ago)

Índice de entradas del conversor


El generador en ZX2SB: diseño, decisiones y límites conscientes

En el proyecto ZX2SB, el generador es el componente encargado de transformar la representación intermedia de un programa ZX BASIC en código SuperBASIC para el Sinclair QL. Aunque pueda parecer que su función consiste únicamente en “emitir líneas de código”, en la práctica es uno de los elementos más importantes y delicados de todo el sistema.

Este artículo describe qué hace realmente el generador, cómo está diseñado, qué problemas resuelve y, sobre todo, qué problemas decide conscientemente no resolver. Esa última decisión resulta clave para entender por qué ZX2SB va más allá de un simple transpilador mecánico .


Índice


1. ¿Qué es el generador?

Conviene recordar una distinción fundamental: un compilador genera un ejecutable, mientras que un transpilador genera código fuente en otro lenguaje, que puede ejecutarse directamente o servir como entrada para otro compilador.

En ZX2SB, el generador actúa como la última fase del transpilador. Recibe una representación intermedia (IR) del programa ZX BASIC, ya validada por el lexer, el parser y el análisis semántico, y produce un programa SuperBASIC funcional y estructurado.

Desde el principio se descartó una traducción línea a línea. El generador trabaja con conocimiento de contexto: estructura, bloques, flujo de control y decisiones previas. No se limita a copiar instrucciones; las reformula

NOTA: 

La “representación intermedia” es una forma neutral de describir el programa una vez entendido sintácticamente, pero antes de decidir cómo escribirlo en el lenguaje de destino. Es como pasar de una frase hablada a su significado real, antes de volver a escribirla en otro idioma.
 

Qué no es ZX2SB

ZX2SB no es un emulador del ZX Spectrum ni una herramienta para ejecutar directamente programas ZX en el Sinclair QL.

Tampoco es un traductor mecánico línea a línea que intente forzar equivalencias entre dos BASIC muy distintos ignorando sus diferencias internas.

ZX2SB es un transpilador consciente: transforma programas ZX BASIC en código SuperBASIC legible, estructurado y extensible, dejando explícitas aquellas partes cuyo comportamiento no puede trasladarse correctamente sin una capa de ejecución adicional.

Su objetivo no es “que funcione como sea”, sino producir código que pueda entenderse, mantenerse y evolucionar en el entorno del QL. 

En esta fase, el objetivo se cumple tal cual, en fases posteriores se puede ampliar para incluir un entorno de simulación del ZXBasic y que el programa se comporte como en un Spectrum en Basic.


2. Generación estructurada de código

Una de las primeras decisiones fue producir código SuperBASIC claramente estructurado, incluso cuando el código ZX original no lo está explícitamente.

En ZX BASIC es habitual encontrar varias sentencias en la misma línea, separadas por dos puntos (:). Por ejemplo:

PRINT A: LET B=B+1 : LET col=col+1
El generador transforma este tipo de líneas en una estructura explícita y mas legible:
  PRINT A
  LET B=B+1
  LET col=col+1

Cada sentencia pasa a ocupar su propia línea. El caso más significativo es el IF. En ZX BASIC no existen ELSE ni END IF, y un IF puede contener múltiples sentencias en una sola línea. El generador transforma esta construcción en un bloque explícito:

  • una línea con la condición IF,
  • una línea por cada sentencia interna,
  • un END IF final.
De esta manera la senténcia
IF A>10: PRINT A: LET B=B+1
genera:
IF A>10 THEN
  PRINT A
  LET B=B+1
END IF

Esta decisión no se tomó por comodidad, sino para garantizar: 

  • Claridad semántica
  • Coherencia estructural
  • Facilidad de mantenimiento
  • Posibilidad de posteriores optimizaciones. 

Muchos programas antiguos funcionan, pero son difíciles de leer incluso para humanos. Aquí el objetivo no es solo “que funcione”, sino que el resultado sea un programa un poco más comprensible y modificable.
 


3. Numeración, control del flujo y bloques

Para poder descomponer líneas múltiples en sentencias independientes, el generador debe modificar la numeración de las líneas.

La estrategia empleada consiste en tomar el número de línea origianl del ZX Basic y añadir un contador de dos dígitos. Así, una línea con varias sentencias se expande de forma determinista.

Por ejemplo, a partir del código ZX:

    150 LET A=3 : LET C=5 : GOTO 532

El generador produce:

    15000 LET A=3
    15001 LET C=5
    15002 GOTO 53200

Estos números no siempre son válidos para el QL (en ZXBasic los números van del 1 al 9999, en SuperBASIC del 1 al 32525), pero esa no es responsabilidad del generador. El renumerador posterior se encargará de normalizarlos.

El generador mantiene contexto suficiente para abrir y cerrar bloques, producir saltos coherentes y dejar el programa en un estado funcional, aunque todavía no directamente cargable.

El generador sí garantiza que:

  • los saltos son coherentes
  • los bloques se abren y cierran correctamente
  • el programa resultante es estructuralmente consistente.

Para el generador la numeración es solo una herramienta provisional. Se usa como andamio mientras se construye el programa final.


4. Instrucciones no portables

ZX BASIC y SuperBASIC difieren profundamente en áreas clave como:

  • Salida de texto (PRINT)
  • Gestión del cursor
  • Colores y atributos
  • Caracteres gráficos
  • Sistema de coordenadas y gráficos

Una traducción directa de estas instrucciones produciría programas que “funcionan”, pero cuyo comportamiento se aleja mucho del ZX Spectrum original.

El generador asume explícitamente que estas instrucciones no son directamente portables y evita resolverlas de forma incorrecta.


5. Convención FN_ y desacoplo

Para manejar instrucciones sin equivalencia directa, el generador adopta una convención clara y sistemática: emitir llamadas a funciones o procedimientos con prefijo FN_

El generador: 

  • No implementa su comportamiento
  • Se limita a transformar el código
  • La semántica se decide posteriormente

Por ejemplo, en lugar de generar directamente:

    BIN 11001101

como el comando BIN no existe en SuperBASIC, el generador produce:

    FN_BIN(11001101)

De este modo, el generador queda completamente desacoplado de la implementación concreta de estas funciones, manteniendo el sistema limpio y extensible.


6. Inicialización e inclusión de funciones FN_

Durante la generación, el sistema mantiene una lista de todas las funciones FN_ que ha utilizado durante el proceso de conversión. Al finalizar la generación, solo se incluyen aquellas funciones que han sido realmente necesarias, evitando así añadir código innecesario al programa final.

Además, el programa resultante se completa con varias secciones fijas, que quedan organizadas de la siguiente manera:

  1. Un bloque de inicio del sistema, que realiza una llamada a la función FN_INIT.
  2. El código convertido desde ZX BASIC a SuperBASIC.
  3. Si es necesario, una línea STOP que marca explícitamente el final del programa ZX.
  4. Las funciones FN_ que han sido detectadas como necesarias durante la generación.
  5. La implementación de FN_INIT

La función FN_INIT se encarga de preparar el entorno de ejecución en el QL: establece el modo de pantalla, crea una ventana de 32×24 caracteres y aplica una configuración básica que permite que el programa se ejecute de forma coherente y predecible.

El STOP final del programa ZX BASIC se añade de forma explícita porque, en BASIC un slto a una línea fuera del rango del programa es válida y provoca implícitamente la finalización del programa. Si se detecta que una línea del programa realzia un salto fuera del rango permitido, se añade esta línea de STOP, cambiando los saltos que existan para que apunten a esta nueva línea. De esta manera:

  • El programa generado se vuelve más determinista y sencillo de manejar
  • Se evitan posibles problemas derivados de que un salto coincida con alguna de las líneas añadidas posteriormente para las funciones FN

7. Qué se obtiene al final

El resultado del generador es un programa SuperBASIC que:

  • Es sintácticamente correcto
  • Preserva la estructura lógica del ZX BASIC original
  • No fuerza equivalencias incorrectas
  • Explicita claramente las dependencias externas

Este programa está listo para ser renumerado, indentado y ejecutado dentro de un entorno controlado.


8. El siguiente paso inmediato

El siguiente paso tras el generador es el renumerador, que ajusta definitivamente la numeración y produce un programa cargable y ejecutable en el QL. Con este proceso de renumerción se cierra la primera fase del proyecto.


9. Más allá del generador

El verdadero reto futuro no está en el generador, sino en lo que rodea a las llamadas FN_....

Para que los programas ZX se comporten de forma reconocible, que parezca que estamos en un gomas real, es necesario proporcionar una capa de ejecución que reproduzca el entorno del Spectrum: pantalla, cursor, colores, caracteres y primitivas gráficas.

Ese componente convierte a ZX2SB en algo más que un transpilador, pero merece un análisis independiente y se abordará en otro momento.


Continuará con el renumerador…


ZB2SB. Paso 3.2. Lexer. Planteamiento

11:52 GMT (5 months ago)
Transpilador ZX BASIC — Paso 1: Analizador léxico (tokens, diagrama, seudo‑código y esqueleto VB/SuperBasic)

Índice de entradas del conversor



Introducción

En una entrada previa presentamos la gramática formal del ZX BASIC del Spectrum 48K. Esa gramática define la estructura sintáctica, pero antes de poder parsear necesitamos convertir el texto en una secuencia estable y tipada de unidades mínimas: los tokens. Esta fase es el analizador léxico o lexer.

El lexer recorre el fichero carácter a carácter, identifica patrones significativos (palabras clave, variables, números, cadenas, operadores…), y los traduce en tokens autoexplicativos que el parser podrá consumir de forma determinista. 

Usaremos la estuctura de datos definida en la entrada anterior para lamcenar los Tokens de la línea y pasarlos a la siguiente fase. 


1. Analizador léxico

  • Convertir texto bruto en una secuencia de tokens bien definidos.
  • Detectar categorías: identificadores, literales, operadores, palabras clave, etc.
  • Emitir la posición exacta (línea y columna) para diagnósticos posteriores.
  • Detectar errores léxicos, continuar cuando sea posible o parar con un mensaje de error adecuado.

Todo esto debe hacerse de manera rápida y predecible, especialmente en nuestro objetivo final: ejecutarlo en un Sinclair QL real, cuyas restricciones de RAM y CPU hacen inviable tokenizar todo el fichero en memoria. Por tanto el lexer trabaja por líneas y emite los tokens de la línea directamente en una estructura compacta optimizada.


2. ¿Qué es un token?

Un token es una unidad mínima y significativa. No representa un carácter, sino un símbolo lógico del lenguaje. Tiene varios atributos:

  • Tipo: categoría (Identifier, Keyword, LineNumber, IntegerLiteral…)
  • Lexema: texto exacto que lo originó
  • Posición: línea y columna
  • Longitud: caracteres consumidos

El parser solo opera sobre tokens, nunca sobre caracteres.


3. Tokens que vamos a generar

3.1 Identificadores y variables

  • Identifier — A, B2, COUNTER
  • StringVar — A$, NAME$

3.2 Literales

  • IntegerLiteral — enteros
  • NumberLiteral — entero, decimal o exponencial
  • StringLiteral — texto entre comillas

3.3 LineNumber

  • Solo válido en columna 1
  • 1 a 4 dígitos, sin 0 inicial

3.4 Operadores

  • +, -, *, /, ^
  • =, <>, <, >, <=, >=
  • AND, OR, NOT

3.5 Separadores

  • ( ) , ; :

3.6 Palabras clave

  • LET, PRINT, INPUT, IF, THEN, FOR, …
  • PLOT, DRAW, CIRCLE, POINT, …
  • READ, DATA, RESTORE, DIM, CLS…
  • REM

3.7 Comentarios

  • Comment — tras REM hasta fin de línea

3.8 Control

  • EOL — fin de línea
  • EOFToken — fin de fichero (si se procesa completo; no usado en QL línea a línea)

4. Diagrama de bloques del analizador léxico

Diagrama de flujo del generador de Tokens
Diagrama de flujo del generador de tokens

4.3 Notas de implementación

  • Normalizar CRLF/CR a \n.
  • Keywords: case‑insensitive, se normalizan a MAYÚSCULAS.
  • LineNumber solo en columna 1.
  • REM: dos variantes — REM vacío o REM␠texto.
  • Prioridad de operadores dobles (<=, <>, >=).
  • Diagnósticos detallados y recuperación local.

5. Lexer en seudo‑código

REM ========================================================
REM  LEXER por líneas — Emite en TokenData$()
REM  Entrada: texto de la línea (linea$), número de línea (numLinea)
REM  Salida : tokens añadidos a TokenData$(), rematados con EOL
REM ========================================================

PROCEDIMIENTO Lexer.Preparar(tamInicial)
    Token.Iniciar()
    Token.Crear(tamInicial)        REM p.ej. 500
    PalabrasClave.Preparar()
FIN

FUNCIÓN Lexer.LexLine(linea$, numLinea) → (idxIni, idxFin)
    idxIni ← TokenCount + 1
    i ← 1
    L ← LEN(linea$)

    MIENTRAS i ≤ L
        REM 1) Espacios
        MIENTRAS i ≤ L Y EsEspacio(MID$(linea$, i, 1))
            i ← i + 1
        FIN MIENTRAS
        SI i > L ENTONCES ROMPER FIN SI

        c$ ← MID$(linea$, i, 1)

        REM 2) Comentario REM (resto de línea)
        SI ComienzaREM(linea$, i) ENTONCES
            lex$ ← MID$(linea$, i)
            Token.Emit(T_REM, lex$, numLinea, i)
            i ← L + 1
            SALIR MIENTRAS
        FIN SI

        REM 3) Cadena
        SI c$ = "\"" ENTONCES
            col ← i
            i ← i + 1
            ini ← i
            MIENTRAS i ≤ L Y MID$(linea$, i, 1) <> "\""
                i ← i + 1
            FIN MIENTRAS
            lex$ ← MID$(linea$, ini, i - ini)     REM sin comillas
            Token.Emit(T_STRING, lex$, numLinea, col)
            SI i ≤ L ENTONCES i ← i + 1 FIN SI
            CONTINUAR MIENTRAS
        FIN SI

        REM 4) Número
        SI EsDigito(c$) ENTONCES
            col ← i
            ini ← i
            MIENTRAS i ≤ L Y EsDigito(MID$(linea$, i, 1))
                i ← i + 1
            FIN MIENTRAS
            esReal ← FALSO
            SI i ≤ L Y MID$(linea$, i, 1) = "." ENTONCES
                esReal ← VERDADERO
                i ← i + 1
                MIENTRAS i ≤ L Y EsDigito(MID$(linea$, i, 1))
                    i ← i + 1
                FIN MIENTRAS
            FIN SI
            lex$ ← MID$(linea$, ini, i - ini)
            SI esReal ENTONCES
                Token.Emit(T_FLOAT, lex$, numLinea, col)
            SINO
                Token.Emit(T_INT,   lex$, numLinea, col)
            FIN SI
            CONTINUAR MIENTRAS
        FIN SI

        REM 5) Identificador / Palabra clave
        SI EsLetra(c$) O c$ = "_" ENTONCES
            col ← i
            ini ← i
            MIENTRAS i ≤ L Y (EsLetraNum(MID$(linea$, i, 1)) O MID$(linea$, i, 1) = "_")
                i ← i + 1
            FIN MIENTRAS
            lex$ ← MID$(linea$, ini, i - ini)
            tipo ← PalabrasClave.Tipo(UPPER$(lex$))
            SI tipo = 0 ENTONCES
                Token.Emit(T_ID, lex$, numLinea, col)
            SINO
                Token.Emit(tipo,  lex$, numLinea, col)
            FIN SI
            CONTINUAR MIENTRAS
        FIN SI

        REM 6) Operadores dobles
        SI i < L ENTONCES
            par$ ← MID$(linea$, i, 2)
            SI par$ = "<=" ENTONCES Token.Emit(T_LE, par$, numLinea, i) : i ← i + 2 : CONTINUAR MIENTRAS FIN SI
            SI par$ = ">=" ENTONCES Token.Emit(T_GE, par$, numLinea, i) : i ← i + 2 : CONTINUAR MIENTRAS FIN SI
            SI par$ = "<>" ENTONCES Token.Emit(T_NE, par$, numLinea, i) : i ← i + 2 : CONTINUAR MIENTRAS FIN SI
        FIN SI

        REM 7) Operadores simples / separadores
        SELECCIONAR c$
            CASO ":" : Token.Emit(T_COLON,  ":", numLinea, i)
            CASO "," : Token.Emit(T_COMMA,  ",", numLinea, i)
            CASO ";" : Token.Emit(T_SEMI,   ";", numLinea, i)
            CASO "(" : Token.Emit(T_LPAREN, "(", numLinea, i)
            CASO ")" : Token.Emit(T_RPAREN, ")", numLinea, i)
            CASO "+" : Token.Emit(T_PLUS,   "+", numLinea, i)
            CASO "-" : Token.Emit(T_MINUS,  "-", numLinea, i)
            CASO "*" : Token.Emit(T_MUL,    "*", numLinea, i)
            CASO "/" : Token.Emit(T_DIV,    "/", numLinea, i)
            CASO "^" : Token.Emit(T_POW,    "^", numLinea, i)
            CASO "=" : Token.Emit(T_EQ,     "=", numLinea, i)
            CASO "<" : Token.Emit(T_LT,     "<", numLinea, i)
            CASO ">" : Token.Emit(T_GT,     ">", numLinea, i)
            OTRO     : Token.Emit(T_ID, c$, numLinea, i)   REM desconocido, se emite tal cual
        FIN SELECCIONAR
        i ← i + 1
    FIN MIENTRAS

    Token.Emit(T_EOL, "", numLinea, L + 1)
    idxFin ← TokenCount
    RETORNAR (idxIni, idxFin)
FIN FUNCIÓN

6. Estructura de datos: arreglo único y registro empaquetado

CHR$(tipo) & MKI$(linea) & MKI$(columna) & CHR$(LEN(lexema$)) & lexema$ (big‑endian)
REM ========= API mínima de Tokens =========

PROCEDIMIENTO Token.Iniciar()
    Estructura_Iniciar(TokenMax, TokenCount)
FIN

PROCEDIMIENTO Token.Crear(tamInicial)
    Estructura_Crear(TokenMax, TokenCount, tamInicial, tipo.Principal)
    DIM TokenData$(TokenMax)
FIN

PROCEDIMIENTO Token.Ampliar(nuevoTamaño)
    Estructura_AmpliarArray(TokenMax, TokenCount, nuevoTamaño)
    AmpliarArray(TokenData$, TokenMax, nuevoTamaño)
    TokenMax ← nuevoTamaño
FIN

PROCEDIMIENTO Token.Reset()
    SI TokenMax = 0 ENTONCES Token.Crear(50)
    TokenCount ← 0
FIN

PROCEDIMIENTO Token.Emit(tipo, lexema$, linea, columna)
    SI TokenMax = 0 ENTONCES Token.Crear(50)
    SI TokenCount >= TokenMax ENTONCES Token.Ampliar(TokenMax + 50)
    TokenCount ← TokenCount + 1
    TokenData$(TokenCount) ← CHR$(tipo) & MKI$(linea) & MKI$(columna) & CHR$(LEN(lexema$)) & lexema$
FIN

REM ==== Acceso a campos de un registro ====
FUNCIÓN Token.GetTipo(tok$) → tipo
    RETORNAR ASC(MID$(tok$,1,1))
FIN

FUNCIÓN Token.GetLinea(tok$) → lin
    RETORNAR CVI(MID$(tok$,2,2))
FIN

FUNCIÓN Token.GetColumna(tok$) → col
    RETORNAR CVI(MID$(tok$,4,2))
FIN

FUNCIÓN Token.GetLongitudLexema(tok$) → n
    RETORNAR ASC(MID$(tok$,6,1))
FIN

FUNCIÓN Token.GetLexema(tok$) → s$
    n ← ASC(MID$(tok$,6,1))
    RETORNAR MID$(tok$,7,n)
FIN

7. El programa generado en Visual Basic .NET

REM Esqueleto QL de lectura por líneas
OPEN #3, "program.bas"
numLinea ← 0
REPEAT
  L$ = LINE INPUT(#3)
  numLinea ← numLinea + 1
  (i0,i1) ← Lexer.LexLine(L$, numLinea)
  REM ... pasar (i0,i1) al parser ...
UNTIL EOF(#3)
CLOSE #3

El proyecto del transpilador (hasta donde esté desarrollado) está en GitHub: ZX2SB.

Notas rápidas

  • EOL normalizado a \n.
  • Keywords en mayúsculas.
  • LineNumber solo en columna 1.
  • REM: variantes válidas documentadas.
  • Números: entero, fracción y exponente.
  • Verbose: opción para depurar mostrando los tokens en pantalla

ZX2SB. Cambios en el planteamiento

11:50 GMT (5 months ago)
Transpilador ZX BASIC a SuperBasic

Índice de entradas del conversor

20/03/2026 Cambios en el esquema de directorios en rojo



Transpilador ZX BASIC → SuperBasic
Dos versiones en paralelo: Moderno y QL

En las últimas entradas hemos avanzado en el diseño interno del transpilador ZX BASIC → SuperBasic, definiendo estructuras de datos, el sistema de tokens y un Lexer optimizado para ejecutarse en un Sinclair QL real. Durante ese proceso me ha surgido una idea interesante para quienes quieran usar este proyecto no solo como herramienta, sino también para aprender sobre compiladores.

Vamos a generar dos versiones del transpilador en paralelo

El objetivo es disponer de dos rutas de ejecución simultáneas, una preparada para migrar a SuperBASIC, y la otra usando elementos actuales. Ambas conviven dentro del mismo proyecto. La mayoría de procesos son comunes, por lo que es sencillo ver cómo pasar de una a otra. Además incluyo una pantalla donde puedes seleccionar cuál quieres lanzar.

1) Versión Moderna

Diseñada para:

  • Usar estructuras avanzadas de .NET (List, Dictionary, etc.).
  • Desarrollo más rápido, depuración más cómoda y diagnósticos completos.
  • Actuar como referencia clara del algoritmo general.

2) Versión compatible con SuperBasic

Implementa las estructuras reales que usará la versión final del QL:

  • TokenBuffer como arreglo dinámico de registros empaquetados.
  • Formato big‑endian de 16 bits, igual que el Motorola 68008.
  • Procesos simples y lineales, estilo SuperBasic.

Esta versión sirve como puente entre .NET moderno y la implementación final en un QL real.

¿Por qué dos versiones?

✔ Didáctica

Muestra cómo implementar un compilador moderno y cómo trasladarlo a una plataforma retro con limitaciones reales.

✔ Validación de algoritmos

Al ejecutar ambos motores en paralelo es fácil encontrar divergencias antes de portar código al QL real.

✔ Documentación y mantenimiento

Permite explicar y probar Lexer, Parser, Semántico y Emitter tanto desde un enfoque moderno como desde un enfoque retro.

Pantalla lanzadora

Incluyo una interfaz WinForms que permite seleccionar:

  • Modo: Moderno o QL‑Prep
  • Ruta y nombre del fichero BASIC
  • Modo Verbose

El lanzador invoca automáticamente el director correspondiente con las opciones necesarias.

Arquitectura del proyecto

El proyecto utiliza .NET Framework 4.8 para asegurar compatibilidad con Windows 7/8/10/11 y aprovechar WinForms clásico. Para quienes pidan Windows XP, 95 o 3.11… lo siento, ¡aún no he retrocedido tanto! (Aunque con lo voluble que soy, cualquiera sabe si un día aparece una versión en Turbo‑C o Turbo Pascal 😄). 

He tenido que ajustar otra vez la estructura de directorios del proyecto, ya que no se generaban las cosas en su lugar correcto, he dividido el proyecto en tres, lanzador, moderno y QL, cada uno es un proyecto independiente que genera su propio EXE, así es mas sencillo manejarlo de manera independiente. 

Estructura general del proyecto:

ZX2SB

├── ZX2SB.sln
├── README.md

├── Lanzador
│ ├── Lanzador.vbproj
│ └── src
│ ├── frmLanzador.vb
│ ├── frmLanzador.Designer.vb
│ ├── frmLanzador.resx
│ └── ... otros

├── Moderno
│ ├── Moderno.vbproj
│ └── src
│ ├── 01_Director.vb
└── ... otros
└── QL
├── QL.vbproj
└── src
├── 01_Director.vb
└── ... otros

Conclusión

A partir de ahora el desarrollo continúa en dos líneas paralelas:

  • Moderno: claridad, pruebas y comodidad.
  • QL‑Prep: compatibilidad y preparación para SuperBasic.

Ambas serán documentadas paso a paso en el blog. Por ahora ya está la estructura base en el repositorio GIT del proyecto, y los directores están preparados para llamar a los procesos (que de momento solo muestran un mensaje en pantalla).

Friday, 17. April 2026

ZX2SB: El Analizador semántico

08:19 GMT (5 months ago)

Qué es un Analizador Semántico y por qué es una pieza clave en el transpilador

En un transpilador o un compilador clásico, el analizador semántico es la fase encargada de comprobar que un programa, además de estar bien escrito desde el punto de vista sintáctico, tiene sentido.

En el proyecto ZX2SB, cuyo objetivo es convertir código ZX BASIC a otro lenguaje, en nuestro caso SuperBASIC del QL, el analizador semántico juega un papel fundamental: es el encargado de interpretar el significado real del programa.


Las fases de un transpilador

Antes de entrar en detalle, conviene situar el analizador semántico dentro del proceso completo:

Código fuente ZX BASIC
        |
        v
  Analizador léxico (Lexer)
        |
        v
 Analizador sintáctico (Parser)
        |
        v
 Analizador semántico (Semantic)
        |
        v
 Generador de código
        |
        v
 Código destino (SuperBASIC)

Cada fase tiene una responsabilidad clara y bien delimitada.

  • Lexer: convierte texto en tokens.
  • Parser: verifica la estructura gramatical.
  • Semantic: verifica el significado del programa.
  • Generator: produce el código destino.

Qué hace exactamente el analizador semántico

El analizador semántico responde a preguntas como:

  • ¿Se usa una variable antes de asignarle un valor?
  • ¿Se asigna una cadena a una variable numérica?
  • ¿Un NEXT corresponde a su FOR?
  • ¿Hay un RETURN sin GOSUB previo?
  • ¿Las instrucciones READ tienen datos DATA suficientes?

En ZX BASIC muchas de estas situaciones están permitidas por el intérprete original, pero al convertir a otros lenguajes conviene detectarlas y, al menos, avisar.


Diseño del analizador semántico en ZX2SB

El analizador semántico de ZX2SB trabaja en dos pasadas sobre un formato intermedio (IR):

  1. Primera pasada: recolección de información.
  2. Segunda pasada: análisis semántico y generación del IR final.

Primera pasada: recolección

En esta fase no se genera código. Se recopila información global:

  • Variables existentes y su tipo (numéricas o de cadena).
  • Uso de instrucciones especiales (PRINT, READ, DATA, AT, etc.).
  • Estructura del programa (FOR/NEXT, GOSUB/RETURN).
  • Mapa de líneas para referencias posteriores.

Esta información se guarda en un contexto semántico que se utiliza en la segunda pasada.

Segunda pasada: análisis y emisión

En la segunda pasada se analiza cada sentencia individualmente:

  • Se validan tipos.
  • Se marcan variables como usadas o asignadas.
  • Se detectan errores y avisos.
  • Se emite el IR normalizado que usará el generador.

Errores y warnings: emisión inmediata

Inicialmente, el analizador semántico acumulaba errores y avisos en listas internas. Sin embargo, en ZX2SB se ha optado por un diseño más simple y coherente:

  • Los errores y warnings se emiten en el momento en que se detectan.
  • La decisión de continuar o abortar se basa en las opciones del usuario.
  • Se eliminan listas auxiliares innecesarias.

Esto unifica el comportamiento con el Lexer, el Parser y el Generator.


Diagrama del flujo del analizador semántico

+-----------------------------+
| Inicializar contexto        |
+-------------+---------------+
              |
              v
+-----------------------------+
| Primera pasada              |
| - Variables                 |
| - DATA / READ               |
| - Estructura                |
+-------------+---------------+
              |
              v
+-----------------------------+
| Segunda pasada              |
| - Analizar sentencias       |
| - Emitir errores/warnings   |
| - Generar IR normalizado    |
+-------------+---------------+
              |
              v
+-----------------------------+
| Resultado semántico         |
+-----------------------------+

Ejemplo de seudocódigo

El siguiente seudocódigo resume el funcionamiento básico:

function EjecutarSemantico():
    inicializar_contexto()

    primera_pasada()
    if error_fatal:
        salir

    segunda_pasada()
    emitir_warnings_variables()

Y el análisis de una sentencia concreta:

function AnalizarLET(sentencia):
    comprobar_formato()
    comprobar_variable()
    comprobar_tipos()
    marcar_asignación()
    emitir_IR()

Por qué es importante esta fase

El analizador semántico es el lugar ideal para:

  • Detectar errores lógicos tempranamente.
  • Dar avisos útiles sin romper compatibilidad con ZX BASIC.
  • Preparar el código para distintos lenguajes destino.
  • Mantener el generador lo más simple posible.

Gracias a esta fase, ZX2SB puede crecer en el futuro hacia nuevos backends (solo cambiando el generador) manteniendo una base sólida.


Conclusión

El analizador semántico es mucho más que una comprobación adicional: es el puente entre la sintaxis y el significado.

En ZX2SB se ha diseñado de forma clara, estructurada y cercana al espíritu de los lenguajes clásicos, facilitando tanto el mantenimiento como la extensión del proyecto.

En próximas entradas profundizaremos en el generador de código y en cómo las decisiones semánticas influyen directamente en el resultado final.

Thursday, 16. April 2026

ZX2SB: El Parser o Analizador Sintáctico

19:04 GMT (5 months ago)

Índice de entradas del conversor


El Analizador Sintáctico (Parser) en un Transpilador ZX BASIC

Cómo funciona el Parser en ZX2SB y por qué es una pieza clave en la conversión de ZX BASIC

Índice


¿Qué es el Analizador Sintáctico (Parser)?

En un transpilador o compilador, el analizador sintáctico, normalmente llamado Parser, es la fase encargada de comprobar que un programa fuente está correctamente estructurado según la gramática del lenguaje.

En el proyecto ZX2SB, el Parser se sitúa entre el analizador léxico y el analizador semántico, actuando como un filtro estructural antes de interpretar el significado real del programa.


El flujo general del transpilador

El proceso de traducción o compilación sigue los pasos que ya hemos visto anteriormente, el parser sería el segundo paso del proceso completo:
 
Código fuente ZX BASIC
|
v
  Analizador léxico (Lexer)
|
v
 Analizador sintáctico (Parser)
|
v
 Analizador semántico
|
v
 Generador de código

Cada fase tiene una responsabilidad clara y no solapa funciones con las demás.

  • Lexer: reconoce palabras, números, cadenas y símbolos.
  • Parser: comprueba la estructura de las sentencias.
  • Semantic: valida el significado y los tipos.
  • Generator: produce el código destino.

Qué hace exactamente un Parser

El analizador sintáctico debe responder a preguntas como estas (pero no todas se aplican al BASIC como veremos más adelante):

  • ¿La sentencia IF tiene un THEN?
  • ¿Un FOR tiene su correspondiente NEXT?
  • ¿La forma general de la instrucción es válida?
  • ¿Los separadores y palabras clave están bien posicionados?

Es importante destacar lo que el Parser NO hace:

  • No comprueba tipos de datos (eso lo hace el analizador semántico).
  • No valida el uso de variables ni su significado.
  • No decide si una operación es lógica o correcta.
  • No genera código final.

Su misión es exclusivamente estructural.


ZX BASIC y la necesidad de un Parser tolerante

ZX BASIC es un lenguaje muy permisivo. Muchas construcciones válidas para el intérprete original pueden resultar ambiguas al convertirlas a otros lenguajes.

Por ese motivo, el Parser de ZX2SB está diseñado para:

  • Aceptar la sintaxis original del ZX Spectrum.
  • Detectar errores estructurales claros.
  • Emitir warnings ante estructuras dudosas.
  • No ser excesivamente restrictivo.

Ejemplo de programa sintácticamente válido pero lógicamente erróneo:


10 FOR i=1 TO 10
20   FOR j=1 TO 5
30     PRINT i,j
40   NEXT i
50 NEXT j

10 FOR i=1 TO 10
20   FOR j=1 TO 5 : PRINT i,j
30 NEXT i

Estos programas no genera error en BASIC clásico, pero su comportamiento no es el esperado y puede variar según el intérprete.


Diseño del Parser en ZX2SB

El Parser trabaja sobre los tokens generados por el Lexer y produce un Árbol de Sintaxis Abstracta (AST).

Dado que el objetivo final es portar el sistema a SuperBASIC, donde no existen estructuras de datos complejas, el AST se almacena como una estructura lineal intermedia (IR).

Entrada

  • Secuencia de tokens.
  • Números de línea originales.
  • Contexto mínimo.

Salida

  • Sentencias normalizadas.
  • Estructura explícita del programa.
  • Errores y avisos.

Diagrama del proceso del Parser

+------------------------+
| Tokens del Lexer       |
+-----------+------------+
            |
            v
+------------------------+
| Reconocer sentencias   |
| (IF, LET, FOR, PRINT)  |
+-----------+------------+
            |
            v
+------------------------+
| Verificar estructura   |
+-----------+------------+
            |
            v
+------------------------+
| Emitir IR estructurado |
+------------------------+

Gestión de errores y warnings

  • Errores: la estructura es inválida.
  • Warnings: la estructura es válida pero sospechosa.

La decisión de continuar o abortar depende de las opciones del usuario que se definan, no del Parser.En nuestro sistema, el usuario puede elegir entre parar y preguntar si desea continuar, o bien no parar el proceso y seguir hasta el final, los errores se verán en el fichero de log que se genera.


Ejemplo de seudocódigo

function EjecutarParser(tokens):
    for cada línea:
        identificar sentencia
        validar estructura
        emitir error o aviso
    generar IR

Relación con el Analizador Semántico

El Parser prepara el terreno para el análisis semántico, permitiendo que este se centre exclusivamente en el significado lógico.


Conclusión

El analizador sintáctico es la columna vertebral estructural del transpilador. Sin interpretar significados, garantiza que estos puedan analizarse correctamente más adelante.

En ZX2SB, el Parser respeta la filosofía del ZX BASIC original y prepara el camino para futuras transformaciones a otros lenguajes.


ZX2SB. Vuelta a empezar

19:03 GMT (5 months ago)

Índice de entradas del conversor



¿De transpilador a compilador?

Evolución del proyecto y cambio de enfoque

Cuando empecé el proyecto de transpilación de ZX BASIC a SuperBASIC (QL), el objetivo parecía claro y razonable: traducir programas clásicos del Spectrum a un SuperBASIC moderno, legible y ampliable, respetando en lo posible el espíritu original pero sin arrastrar las limitaciones del intérprete de los años 80.

Al principio pensaba que sería relativamente sencillo: tokenizar el programa y traducirlo a un lenguaje muy similar. Sin embargo, paso a paso el proyecto fue complicándose. Empezaron a aparecer casos especiales, decisiones incómodas y pequeñas incoherencias que, aunque no impedían avanzar, sí dificultaban hacerlo bien.

Sin embargo, como suele ocurrir en los proyectos que merecen la pena, el camino no fue lineal. Lo que empezó como un “simple transpilador” acabó convirtiéndose en algo conceptualmente más ambicioso: una cadena de compilación modular, con fases bien definidas, contratos explícitos y decisiones arquitectónicas conscientes.


El punto de partida: traducir, no reinterpretar

Desde el principio hubo varias decisiones claras:

  • El código generado debía ser SuperBASIC editable, no un artefacto opaco.
  • No se intentaría emular el ZX Spectrum al 100 %:
    • nada de PEEK / POKE
    • nada de trucos de automodificación
  • Las funciones ZX no soportadas se resolverían mediante llamadas a rutinas auxiliares escritas en SuperBASIC.
  • El resultado debía ser extensible, pensando en mejoras futuras.

El transpilador original funcionaba como un bloque monolítico: leer una línea, analizarla, transformarla y generar código QL.

Funcionaba, pero conforme el proyecto crecía, también crecían las dudas.


El generador fue el primer aviso

El proyecto avanzaba, con cambios en el lexer y el parser por el camino, pero fue el generador el que puso el primer escalón incómodo delante del diseño.

Para generar buen SuperBASIC había que responder a preguntas que un “transpilador rápido” no suele plantearse:

  • ¿Cómo se numeran las líneas para facilitar la edición manual?
  • ¿Dónde debe ir la inicialización del entorno gráfico?
  • ¿Cómo se insertan subrutinas (PROC) sin romper el flujo?
  • ¿Qué partes del código son infraestructura y cuáles lógica del programa?

Si el generador necesita un AST claro, el resto del sistema también debería pensarse como un compilador.

Reescribir y replantear no es perder tiempo: es ganar claridad, mantenibilidad y, muchas veces, velocidad.


La ruptura conceptual: dividir en módulos

El diseño monolítico, pensado para optimizar el QL, terminó siendo un error. La decisión clave fue:

Dividir el sistema en módulos independientes, comunicados solo por ficheros.

  1. Director: orquesta el proceso, sin conocer el lenguaje.
  2. Lexer: reconoce ZX BASIC y genera tokens.
  3. Parser: construye el AST.
  4. Semántico: valida y ajusta el AST.
  5. Generador: produce SuperBASIC editable.

Primer paso: el fichero .TOK

El fichero .tok es la frontera definitiva entre fases:

  • texto plano
  • una línea por token
  • LINE, EOL, EOF
  • sin posiciones ni contexto oculto
  • la única fuente de verdad para el parser
LINE 50
Keyword IF
Identifier C
OpMayorIgual >=
Number 20
Keyword THEN
Keyword GOTO
Number 100
EOL

Si algo no está en el .tok, el parser no lo sabe.


El papel del Director: menos es más

El Director no debe saber si el lenguaje es ZX BASIC, C o Pascal.

El lexer reconoce y valida el número de línea. El Director solo coordina.


El resultado: un lexer cerrado

  • Reconoce ZX BASIC correctamente
  • Detecta errores léxicos reales
  • No adelanta semántica
  • Genera un .tok definitivo

Esta fase está cerrada.


Conclusión

El proyecto ya no es solo un transpilador: es una base sólida para un posible compilador real.

El lexer está cerrado, el contrato existe, y el camino es ahora mucho más claro.


Postdata

Este proyecto es complejo y formativo. Revisar, dudar, reescribir y aprender forma parte esencial del proceso.

La ayuda de herramientas como Copilot ha sido clave, siempre revisando, cuestionando y aprendiendo.

Sunday, 15. March 2026

ZB2SB. Paso 3.1. Lexer. Estructura de datos para manejo de Tokens

21:41 GMT (6 months ago)
Transpilador ZX BASIC para Sinclair QL: Optimización del Lexer (Fase 1)

Índice de entradas del conversor



Introducción

Para ejecutar el transpilador ZX BASIC → SuperBasic en un Sinclair QL real debemos optimizar al máximo la memoria y la velocidad. Por ello, el Lexer usará un único arreglo de cadena donde cada elemento es un token en un registro empaquetado (formato binario compacto), en lugar de varios arreglos paralelos. Esto reduce la fragmentación de memoria, simplifica las ampliaciones y acelera copias.

El objetivo de esta entrada es implementar, en seudo‑código, todas las funciones necesarias para manejar la estructura de Tokens (crear, ampliar, resetear, insertar y leer) y añadir funciones de prueba que crean registros y los verifican. Esta entrada es en seudo‑código; el desarrollo real en Visual Basic se publicará en el repositorio del proyecto.


1. Diseño del registro de Token (formato binario)

Formato de registro (TokenRecord):
CHR$(tipo) & MKI$(linea) & MKI$(columna) & CHR$(LEN(lexema$)) & lexema$
  • tipo → byte (0..255).
  • línea → entero sin signo de 16‑bit (2 bytes).
  • columna → entero sin signo de 16‑bit (2 bytes).
  • len(lexema$) → byte (0..255).
  • lexema$ → cadena (longitud variable).

2. Funciones MKI$ y CVI (seudocódigo compatible QL)

Aclaración sobre endianness: Para almacenar números de más de 1 byte se usan dos sistemas diferentes, según el orden en que se guardan en memoria (o en la cadena, en nuestro caso), se denomina MSB al byte más significativo del conjunto de bytes (en un número sería la parte izquierda) y LSB al menos significativo (en un número la parte derecha):

  • Big‑endian (primero el byte más significativo, MSB): el número se mantiene en el orden “natural” de sus bytes, es decir, en binario puro con el MSB antes que el LSB. Es el formato nativo del 68008 (y toda la familia 680x0), y por tanto el que usa el QL. También lo emplearon arquitecturas como SPARC (en muchas implementaciones), PowerPC (en configuraciones clásicas de servidores/Unix) y ciertos sistemas IBM de gran porte. Es el formato que usaremos en nuestros procesos.
  • Little‑endian (primero el byte menos significativo, LSB): el número se almacena con el orden de bytes invertido. Aunque pueda parecer menos intuitivo, favorece algunas operaciones aritméticas con acarreo al comenzar por el LSB y propagar el acarreo hacia los bytes más altos. Es el formato usado en los PC con Intel y también el predeterminado en la mayoría de sistemas ARM y RISC‑V actuales, y fue el usado en los DEC PDP .
REM ==================================================================
REM  MKI$(n) — Devuelve una cadena de 2 bytes a partir de un entero
REM            de 16 bits sin signo, en formato big-endian (MSB, LSB)
REM ==================================================================
FUNCIÓN MKI$(n) → s$
    SI n < 0 ENTONCES ERROR No se soportan negativos
    SI n > 65535 ENTONCES ERROR Número demasiado grande

    hi ← (n \ 256) MOD 256
    lo ← n MOD 256

    s$ ← CHR$(hi) & CHR$(lo)
    RETORNAR s$
FIN FUNCIÓN

REM ==================================================================
REM  CVI(s$) — Convierte una cadena de 2 bytes (big-endian)
REM            en un entero de 16 bits sin signo
REM ==================================================================
FUNCIÓN CVI(s$) → n
    hi ← ASC(MID$(s$,1,1))
    lo ← ASC(MID$(s$,2,1))
    n  ← hi*256 + lo
    RETORNAR n
FIN FUNCIÓN

3. API de gestión de Tokens

REM ============================================================
REM  TOKENS — Arreglo único de cadenas con registros empaquetados
REM  TokenData$(n), TokenCount, TokenMax
REM ============================================================

PROCEDIMIENTO Token.Iniciar()
    Estructura_Iniciar(TokenMax, TokenCount)   REM Max=0, Count=0
FIN

PROCEDIMIENTO Token.Crear(tamInicial)
    Estructura_Crear(TokenMax, TokenCount, tamInicial, tipo.Principal)
    DIM TokenData$(TokenMax)
FIN

PROCEDIMIENTO Token.Ampliar(nuevoTamaño)
    Estructura_AmpliarArray(TokenMax, TokenCount, nuevoTamaño)
    AmpliarArray(TokenData$, TokenMax, nuevoTamaño)
    TokenMax ← nuevoTamaño
FIN

PROCEDIMIENTO Token.Reset()
    SI TokenMax = 0 ENTONCES Token.Crear(50)
    TokenCount ← 0
FIN

4. Empaquetado y acceso a los datos del registro

REM =========================================
REM  Empaquetado / Acceso a campos del token
REM =========================================

FUNCIÓN Token.Encode(tipo, linea, columna, lexema$) → token$
    token$ ← CHR$(tipo) & MKI$(linea) & MKI$(columna) & CHR$(LEN(lexema$)) & lexema$
    RETORNAR token$
FIN

FUNCIÓN Token.GetTipo(token$) → tipo
    tipo ← ASC(MID$(token$, 1, 1))
    RETORNAR tipo
FIN

FUNCIÓN Token.GetLinea(token$) → linea
    linea ← CVI(MID$(token$, 2, 2))
    RETORNAR linea
FIN

FUNCIÓN Token.GetColumna(token$) → col
    col ← CVI(MID$(token$, 4, 2))
    RETORNAR col
FIN

FUNCIÓN Token.GetLongitudLexema(token$) → lenLex
    lenLex ← ASC(MID$(token$, 6, 1))
    RETORNAR lenLex
FIN

FUNCIÓN Token.GetLexema(token$) → lex$
    lenLex ← ASC(MID$(token$, 6, 1))
    lex$ ← MID$(token$, 7, lenLex)
    RETORNAR lex$
FIN

5. Alta de nuevos tokens en el arreglo

REM ==========================
REM  Alta (añadir) de un token
REM ==========================
PROCEDIMIENTO Token.Emit(tipo, lexema$, linea, columna)
    SI TokenMax = 0 ENTONCES Token.Crear(50)
    SI TokenCount >= TokenMax ENTONCES Token.Ampliar(TokenMax + 50)

    TokenCount ← TokenCount + 1
    TokenData$(TokenCount) ← Token.Encode(tipo, linea, columna, lexema$)
FIN

6. Prueba: crear registros, recuperarlos y mostrarlos

REM ============================================================
REM  PRUEBAS — Director
REM   • CLS al inicio
REM   • CrearCasos una sola vez
REM   • Emite, verifica (PRUEBA 1)
REM   • Corrompe 2 tokens ya emitidos
REM   • Muestra "=== PRUEBA 2 ===" y verifica (errores)
REM ============================================================
PROCEDIMIENTO Pruebas()
    CLS

    CrearCasos()

    REM Emisión inicial desde la base
    Token.Iniciar()
    Token.Crear(MAX(NCasos, 4))
    PARA i ← 1 HASTA NCasos
        Token.Emit(BaseTipo(i), BaseLex$(i), BaseLinea(i), BaseCol(i))
    FIN PARA

    PRINT "=== PRUEBA 1 ==="
    REM PRIMERA VERIFICACIÓN (OK)
    Verificar()

    REM Corromper sin tocar la base de esperados:
    REM  i=2: lin=99
    REM  i=5: col=99
    TokenData$(2) ← Token.Encode(BaseTipo(2), 99, BaseCol(2), BaseLex$(2))
    TokenData$(5) ← Token.Encode(BaseTipo(5), BaseLinea(5), 99, BaseLex$(5))

    PRINT
    PRINT "=== PRUEBA 2 ==="

    REM SEGUNDA VERIFICACIÓN (debe mostrar 2 errores)
    Verificar()
FIN PROCEDIMIENTO


REM ============================================================
REM  CrearCasos — Define los casos base (solo se llama una vez)
REM ============================================================
PROCEDIMIENTO CrearCasos()
    DIM BaseTipo(50)
    DIM BaseLex$(50)
    DIM BaseLinea(50)
    DIM BaseCol(50)
    NCasos ← 0

    NCasos ← NCasos + 1
    BaseTipo(NCasos)  ← 10
    BaseLex$(NCasos)  ← "PRINT"
    BaseLinea(NCasos) ← 100
    BaseCol(NCasos)   ← 1

    NCasos ← NCasos + 1
    BaseTipo(NCasos)  ← 1
    BaseLex$(NCasos)  ← "A"
    BaseLinea(NCasos) ← 100
    BaseCol(NCasos)   ← 7

    NCasos ← NCasos + 1
    BaseTipo(NCasos)  ← 30
    BaseLex$(NCasos)  ← "+"
    BaseLinea(NCasos) ← 100
    BaseCol(NCasos)   ← 9

    NCasos ← NCasos + 1
    BaseTipo(NCasos)  ← 2
    BaseLex$(NCasos)  ← "42"
    BaseLinea(NCasos) ← 100
    BaseCol(NCasos)   ← 11

    NCasos ← NCasos + 1
    BaseTipo(NCasos)  ← 20
    BaseLex$(NCasos)  ← ":"
    BaseLinea(NCasos) ← 100
    BaseCol(NCasos)   ← 13

    NCasos ← NCasos + 1
    BaseTipo(NCasos)  ← 4
    BaseLex$(NCasos)  ← "HELLO"
    BaseLinea(NCasos) ← 101
    BaseCol(NCasos)   ← 1

    NCasos ← NCasos + 1
    BaseTipo(NCasos)  ← 21
    BaseLex$(NCasos)  ← ","
    BaseLinea(NCasos) ← 101
    BaseCol(NCasos)   ← 2
FIN PROCEDIMIENTO


REM ============================================================
REM  Verificar — Solo lectura y comprobación
REM  (no crea, no emite; compara almacenamiento vs. base)
REM  Salida en varias líneas por elemento para evitar desbordes
REM ============================================================
PROCEDIMIENTO Verificar()
    PRINT "=== Verificación de tokens (NCasos="; NCasos; ") ==="

    fallos ← 0
    hasta ← MIN(TokenCount, NCasos)

    PARA i ← 1 HASTA hasta
        tok$   ← TokenData$(i)
        tipo   ← Token.GetTipo(tok$)
        lin    ← Token.GetLinea(tok$)
        col    ← Token.GetColumna(tok$)
        lenLex ← Token.GetLongitudLexema(tok$)
        lex$   ← Token.GetLexema(tok$)

        expT   ← BaseTipo(i)
        expL$  ← BaseLex$(i)
        expLi  ← BaseLinea(i)
        expCo  ← BaseCol(i)

        esOK ← (tipo = expT) Y (lin = expLi) Y (col = expCo) Y (lenLex = LEN(expL$)) Y (lex$ = expL$)

        SI esOK ENTONCES
            PRINT i; ": OK  ->"
            Ver(tipo, lin, col, lenLex, lex$)
        SINO
            fallos ← fallos + 1
            PRINT i; ": ERR ->  ESP:"
            Ver(expT, expLi, expCo, LEN(expL$), expL$)
            PRINT "               OBT:"
            Ver(tipo, lin, col, lenLex, lex$)
        FIN SI
    FIN PARA

    SI fallos = 0 ENTONCES
        PRINT "Resultado: OK ( "; hasta; " registros verificados )"
    SINO
        PRINT "Resultado: ERROR ( "; fallos; " fallos de "; hasta; " )"
    FIN SI
FIN PROCEDIMIENTO

REM ============================================================
REM  Ver — Muestra los campos del registro en líneas separadas
REM ============================================================
PROCEDIMIENTO Ver(tipo, lin, col, lenLex, lex$)
    PRINT "  tipo="; tipo
    PRINT "  lin="; lin
    PRINT "  col="; col
    PRINT "  len="; lenLex
    PRINT "  lex=["; lex$; "]"
FIN PROCEDIMIENTO

7. Siguiente paso

  • Integrar estos cambios en el Lexer real (Fase 2): sustituir escrituras paralelas por Token.Emit y usar TokenData$() en el Parser con Token.Get*.
  • Publicar el módulo equivalente en VB.NET (Encode/Decode + pruebas), con BitConverter e inversión a big‑endian.

Saturday, 14. March 2026

ZB2SB. Paso Auxiliar 1. Estructuras de datos

07:05 GMT (6 months ago)
Transpilador ZX BASIC — Estructuras de datos para el transpilador (y adaptación a SuperBasic)

Índice de entradas del conversor


Introducción

El transpilador ZX BASIC → SuperBasic que estamos desarrollando debe ejecutarse finalmente en un Sinclair QL real. Esto significa trabajar con:

  • 128 KB de RAM totales (menos aún disponibles para SuperBasic),
  • sin estructuras dinámicas modernas como listas, diccionarios o árboles,
  • sin objetos ni clases,
  • E/S muy lenta (microdrives).

Por ese motivo, todas las estructuras de datos que en VB.NET son naturales (List<T>, diccionarios, árboles, AST reales…) deben redefinirse desde cero para poder implementarse en SuperBasic.

Esta entrada documenta todas las estructuras que necesitaremos en el transpilador y proporciona un seudo‑código portable al QL. Si más adelante fuera necesario añadir otras, se hará en otra entrada.

En VB.NET usamos listas y clases por comodidad y velocidad, pero en el QL se implementará todo con los recursos disponibles. Además, esta filosofía exige encapsular las estructuras, lo cual es una buena práctica de diseño. Para mantener precisión terminológica: hablaré de arreglo unidimensional (o simplemente arreglo) para lo que habitualmente se denomina array, de arreglo multidimensional (2 o más dimensiones) para lo que habitualmente se llama Matriz para 2 dimensiones, y Matriz de n dimensiones cuando hay mas de 2 (en matemáticas, un arreglo de 3 o más dimensiones se denomina tensor).


1. Por qué necesitamos estructuras propias

SuperBasic no dispone de:

  • listas dinámicas,
  • arreglos redimensionables sin pérdida,
  • tipos compuestos o registros,
  • pilas, colas o diccionarios,
  • árboles.

La única herramienta fiable son los arreglos estáticos. Por ello cada estructura del transpilador debe implementarse como arreglos más unas variables auxiliares. Los arreglos podrán crecer si es necesario.

2. Qué es un registro

En programación y en ingeniería de datos, un registro es una unidad lógica de información formada por varios campos, cada uno con un significado propio. Un registro no es un arreglo, sino un agrupador de datos relacionados que conceptualmente funcionan como una sola unidad.

En lenguajes modernos un registro sería equivalente a una “estructura” o “struct”, pero en SuperBasic del QL no existen tipos compuestos: solo disponemos de arreglos numéricos y de cadenas. Por tanto, cuando hablamos de un “registro” en este transpilador, nos referimos a un concepto abstracto que representamos usando varios arreglos paralelos o empleando un único arreglo de cadenas que contiene todos los campos empaquetados.

2.1 Campos dentro de un registro

Un registro está formado por varios valores individuales (campos). Por ejemplo, un registro para un token contiene campos como:

  • tipo del token,
  • lexema (cadena original),
  • línea donde aparece,
  • columna dentro de esa línea,
  • longitud del lexema.

Todos esos campos juntos forman un solo registro Token.

2.2 Cómo representaremos un registro

Dado que SuperBasic no permite definir estructuras complejas, podemos representar un registro de dos formas posibles:

  • Mediante varios arreglos paralelos: cada campo del registro se guarda en un arreglo independiente, y el índice n identifica el mismo registro en todos ellos.
  • Mediante un único arreglo de cadena con los datos empaquetados: cada elemento del arreglo contiene todos los campos del registro codificados en una sola cadena, usando separadores o marcas de longitud.

Ambos métodos expresan el mismo concepto: un registro es un conjunto de campos que pertenecen juntos, y que se accede siempre por un mismo índice. El formato exacto de representación dependerá de la estructura (Tokens, Diagnósticos, Tabla de líneas, etc.).

Con esta definición, cuando hablemos de “crear un registro”, “ampliar un registro” o “acceder a un registro”, nos referiremos siempre a estas unidades lógicas que agrupamos en una cadena y almacenamos mediante arreglos.

2.3 Formatos de empaquetado para registros

Para representar un registro dentro de un arreglo de cadenas en SuperBasic, existen varias técnicas de empaquetado. Cada una equilibra de forma distinta la ocupación en memoria, la velocidad de acceso y la facilidad de decodificación. En el transpilador adoptaremos tres estilos según la estructura a almacenar.

  • A) Separadores entre campos (p. ej. "3|A$|120|5|2"): legible y sencillo, adecuado para estructuras poco críticas en rendimiento.
  • B) Campos de tamaño fijo: rápido de extraer mediante MID$, aunque menos flexible y con espacio desaprovechado.
  • C) Longitud + campo (formato binario compacto):
    CHR$(tipo) & MKI$(linea) & MKI$(col) & CHR$(LEN(lex$)) & lex$
    Es el método más rápido y más eficiente para estructuras que se consultarán miles de veces.

En este artículo, para cada estructura del transpilador indicaré explícitamente el formato de registro que utilizará, detallando los nombres de los campos, su tipo y cómo se empaquetan en la cadena final.

3. Almacenamiento de cadenas en el QL real

El SuperBasic del Sinclair QL no almacena cadenas dentro del arreglo como hacía ZX BASIC (espacio fijo por elemento), ni usa arreglos de punteros al estilo C. Cada elemento de un arreglo de cadenas contiene un descriptor (puntero + longitud), y el texto vive en el heap dinámico del QDOS.

3.1 ¿Qué hay realmente en un arreglo de cadenas?

Cuando declaramos un arreglo de cadenas como:

DIM A$(9)
A$(0) = "HOLA"
A$(1) = "QL"
A$(2) = ""
A$(3) = "TRANS"

El QL reserva 10 descriptores (índices 0 a 9), y cada descriptor contiene dos valores:

  • un puntero a la cadena real almacenada en el heap del QDOS,
  • la longitud de esa cadena.

Por tanto, tras las asignaciones, la estructura interna del arreglo es:

A$(0)  = descriptor (puntero, 4)  → "HOLA"
A$(1)  = descriptor (puntero, 2)  → "QL"
A$(2)  = descriptor (puntero, 0)  → ""
A$(3)  = descriptor (puntero, 5)  → "TRANS"
A$(4)  = descriptor (puntero, 0)  → ""
A$(5)  = descriptor (puntero, 0)  → ""
A$(6)  = descriptor (puntero, 0)  → ""
A$(7)  = descriptor (puntero, 0)  → ""
A$(8)  = descriptor (puntero, 0)  → ""
A$(9)  = descriptor (puntero, 0)  → ""

Obsérvese que ninguna cadena se guarda dentro del arreglo. Solo se almacena un descriptor por elemento; el texto vive siempre en el heap, lo que permite ampliar o copiar arreglos de cadenas de forma rápida (solo se copian punteros).


4. Comparativa de modelos de estructuras en SuperBasic

Existen varias maneras diferentes de almacenar los valores de nuestras estructuras en arreglos dentro del QL:

4.1 Varios arreglos paralelos

  • Qué es: un arreglo por cada campo de la estructura (p. ej., TokenType(), TokenLexema$()…).
  • Ventajas: acceso rápido; depuración clara.
  • Inconvenientes: hay que ampliar/copiar varios arreglos; más fragmentación con muchos campos de texto.
  • Uso: recomendable para AST y estructuras con enlaces.

4.2 Arreglo multidimensional

  • Qué es: DIM Datos(N, M) (numérico).
  • Límite: con cadenas no resuelve el problema; seguiría siendo necesario otro arreglo $.
  • Uso: no recomendado para nuestras estructuras con texto.

4.3 Un único arreglo de cadena con los datos empaquetados

  • Qué es: Datos$() donde cada elemento empaqueta campos (tipo, línea, col., lexema…).
  • Ventajas: se amplía y copia un solo arreglo; menos fragmentación.
  • Inconvenientes: hay que empaquetar/desempaquetar; ligera penalización de CPU.
  • Uso: muy adecuado para Tokens, Diagnósticos, Símbolos, Tabla de líneas y Salida.

5. Estructuras de datos del transpilador

Las estructuras necesarias son:

  • Lista de Tokens — salida del Lexer.
  • Árbol de Sintaxis Abstracta (AST) — salida del Parser.
  • Tabla de Líneas — usada por el Semántico.
  • Tabla de Símbolos — variables y arreglos.
  • Tabla de Diagnósticos — errores por fases.
  • Buffer de Salida — líneas SuperBasic generadas.

Todas deben existir en VB.NET y en SuperBasic, con forma conceptual idéntica.


6. Descripción detallada de cada estructura

En un QL real debemos optimizar al máximo memoria y tiempo de acceso. Por ello, en cada estructura emplearemos la mejor representación posible: registros empaquetados en un único arreglo de cadena cuando primen eficiencia y sencillez, y arreglos paralelos cuando la estructura sea jerárquica (AST) y requiera enlaces explícitos entre nodos.


6.1 Tokens (Lexer)

6.1.1 Representación

Un único arreglo de cadenas empaquetadas (formato binario compacto) por token:

TokenData$(n)
TokenCount
TokenMax

6.1.2 Registro: TokenRecord

Campo        : Tipo
Tipo de dato : byte (0..255)
Descripción  : Categoría del token (Identifier, Keyword, etc.)
Almacenado   : CHR$(tipo)

Campo        : Línea
Tipo de dato : entero corto (2 bytes)
Descripción  : Nº de línea física en el fichero de entrada
Almacenado   : MKI$(linea)

Campo        : Columna
Tipo de dato : entero corto (2 bytes)
Descripción  : Columna (1‑based) donde comienza el token
Almacenado   : MKI$(columna)

Campo        : LongitudLexema
Tipo de dato : byte (0..255)
Descripción  : Longitud del lexema en caracteres
Almacenado   : CHR$(LEN(lexema$))

Campo        : Lexema
Tipo de dato : cadena (longitud variable)
Descripción  : Texto literal del token
Almacenado   : lexema$

6.1.3 Empaquetado

TokenData$(n) =
  CHR$(tipo) &
  MKI$(linea) &
  MKI$(columna) &
  CHR$(LEN(lexema$)) &
  lexema$

6.2 AST por línea (Parser)

6.2.1 Representación

El AST (línea, sentencias y expresiones) es una estructura jerárquica. Para mantener los enlaces entre nodos y recorrer el “árbol” con claridad y rapidez en QL, se usarán varios arreglos paralelos.

6.2.2 Nodo de Línea

ASTLine_Number(id)     → Nº de línea ZX BASIC
ASTLine_StmtIndex(id)  → Primer índice en Stmt*
ASTLine_StmtCount(id)  → Nº de sentencias en la línea
ASTLineCount           → Nº real de nodos de línea
ASTLineMax             → Capacidad reservada

6.2.3 Sentencias

StmtType(n)            → Tipo de sentencia (LET, PRINT, IF, ...)
StmtParam1$(n)         → Parámetro textual principal (p. ej. destino en LET)
StmtParam2$(n)         → Segundo parámetro textual (si aplica)
StmtExprIndex(n)       → Índice en Expr* de la expresión asociada (0 si no hay)
StmtCount              → Nº real de sentencias
StmtMax                → Capacidad reservada

6.2.4 Expresiones

ExprType(n)            → Literal, VarRef, Unary, Binary, FuncCall, Paren...
ExprValue$(n)          → Valor textual asociado (variable, número, nombre)
ExprLeft(n)            → Índice de subexpresión izquierda (0 si no aplica)
ExprRight(n)           → Índice de subexpresión derecha (0 si no aplica)
ExprCount              → Nº real de expresiones
ExprMax                → Capacidad reservada

6.3 Tabla de Líneas (Semántico)

6.3.1 Representación

Un único arreglo de cadenas con separadores (legible y suficiente en rendimiento):

LineData$(n)   = numeroLinea$ & "|" & astIndex$
LineCount
LineMax

6.3.2 Registro: LineRecord

Campo        : NumeroLinea
Tipo de dato : entero (en texto)
Descripción  : Nº de línea ZX BASIC existente en el programa
Almacenado   : "1234"

Separador1   : "|"
Tipo de dato : carácter (longitud fija = 1)
Descripción  : Separador de campos
Almacenado   : "|"

Campo        : ASTIndex
Tipo de dato : entero (en texto)
Descripción  : Índice en las tablas ASTLine_* para esa línea
Almacenado   : "57"

6.4 Tabla de Símbolos

6.4.1 Representación

Un único arreglo de cadenas con separadores:

SymbolData$(n) = nombre$ & "|" & tipo$
SymbolCount
SymbolMax

6.4.2 Registro: SymbolRecord

Campo        : Nombre
Tipo de dato : cadena (longitud variable)
Descripción  : Identificador del símbolo (A, A$, ARR(), ...)
Almacenado   : "A$"

Separador1   : "|"
Tipo de dato : carácter (longitud fija = 1)
Descripción  : Separador de campos
Almacenado   : "|"

Campo        : Tipo
Tipo de dato : cadena corta
Descripción  : Clasificación (p. ej. "NUM", "STR", "ARR_NUM", "ARR_STR")
Almacenado   : "STR"

6.5 Diagnósticos

6.5.1 Representación

Un único arreglo de cadenas con separadores:

DiagData$(n) = mensaje$ & "|" & linea$ & "|" & columna$ & "|" & fase$
DiagCount
DiagMax

6.5.2 Registro: DiagRecord

Campo        : Mensaje
Tipo de dato : cadena (longitud variable)
Descripción  : Texto del diagnóstico (error/aviso)
Almacenado   : "Invalid token '£'"

Separador1   : "|"
Tipo de dato : carácter
Descripción  : Separador
Almacenado   : "|"

Campo        : Línea
Tipo de dato : entero (en texto)
Descripción  : Nº de línea física (0 si global)
Almacenado   : "123"

Separador2   : "|"
Tipo de dato : carácter
Descripción  : Separador
Almacenado   : "|"

Campo        : Columna
Tipo de dato : entero (en texto)
Descripción  : Columna asociada (0 si no aplica)
Almacenado   : "17"

Separador3   : "|"
Tipo de dato : carácter
Descripción  : Separador
Almacenado   : "|"

Campo        : Fase
Tipo de dato : cadena corta
Descripción  : "Lexer", "Parser", "Semántico", "Transformador", "Emisor"
Almacenado   : "Lexer"

6.6 Buffer de salida

6.6.1 Representación

Un único arreglo de cadenas donde cada elemento es la línea destino completa:

Output$(n)    = lineaSuperBasic$
OutputCount
OutputMax

6.6.2 Registro: OutputRecord

Campo        : LineaSB
Tipo de dato : cadena (longitud variable)
Descripción  : Línea completa ya generada en SuperBasic
Almacenado   : "100 PRINT \"HOLA\""

7. Seudo‑código de las estructuras

7.0 Funciones genéricas para usar arreglos como lista

Seudo‑código que expresa el comportamiento lógico (no es SuperBasic literal):

// ======================================================================
//  FUNCIONES AUXILIARES GENÉRICAS PARA TODAS LAS ESTRUCTURAS
// ======================================================================

// Reset de estructura (mantiene arreglos, Count=0)
PROCEDIMIENTO Estructura_Reset(var Count)
    Count ← 0
FIN

// Crear estructura con tamaño inicial
PROCEDIMIENTO Estructura_Crear(var Max, var Count, tamInicial, tipo)
    SI tipo = tipo.Principal ENTONCES
        Crear_Arrays(tamInicial)  // Arreglos definitivos
        Max   ← tamInicial
        Count ← 0
    SINO
        Crear_ArraysAuxiliares(tamInicial)  // Arreglos temporales
    FIN SI
FIN

// Ampliar estructura a un nuevo tamaño absoluto
PROCEDIMIENTO Estructura_AmpliarArray(var Max, var Count, nuevoTamaño)
    SI nuevoTamaño <= Max ENTONCES
        RETORNAR
    FIN SI

    // Crear arreglos auxiliares y copiar
    Estructura_Crear(auxMax, auxCount, nuevoTamaño, tipo.Auxiliar)
    Copiar_Arrays(array_Actual, array_Temporal)

    // Crear nuevos arreglos principales ampliados y restaurar
    Estructura_Crear(Max, Count, nuevoTamaño, tipo.Principal)
    Copiar_Arrays(array_Temporal, array_Actual)

    Max ← nuevoTamaño
FIN

7.X Firmas de funciones por estructura (sin implementación)

// TOKENS
PROCEDIMIENTO Token_Iniciar()
PROCEDIMIENTO Token_Crear(tamInicial)
PROCEDIMIENTO Token_Ampliar(nuevoTamaño)
PROCEDIMIENTO Token_Reset()
PROCEDIMIENTO Token_Add(tipo, lexema$, linea, columna, longitud)

// AST
PROCEDIMIENTO AST_Iniciar()
PROCEDIMIENTO AST_Crear(tamInicial)
PROCEDIMIENTO AST_Ampliar(nuevoTamaño)
PROCEDIMIENTO AST_Reset()
FUNCIÓN       AST_NewLine(lineNumber) → idLinea
FUNCIÓN       AST_AddStatement(idLinea, tipoStmt) → idStmt

// SENTENCIAS
PROCEDIMIENTO Stmt_Iniciar()
PROCEDIMIENTO Stmt_Crear(tamInicial)
PROCEDIMIENTO Stmt_Ampliar(nuevoTamaño)
PROCEDIMIENTO Stmt_Reset()
FUNCIÓN       Stmt_Add(tipoStmt, param1$, param2$, exprIndex) → idStmt

// EXPRESIONES
PROCEDIMIENTO Expr_Iniciar()
PROCEDIMIENTO Expr_Crear(tamInicial)
PROCEDIMIENTO Expr_Ampliar(nuevoTamaño)
PROCEDIMIENTO Expr_Reset()
FUNCIÓN       Expr_Add(tipoExpr, valor$, leftIndex, rightIndex) → idExpr

// TABLA DE LÍNEAS
PROCEDIMIENTO LineTable_Iniciar()
PROCEDIMIENTO LineTable_Crear(tamInicial)
PROCEDIMIENTO LineTable_Ampliar(nuevoTamaño)
PROCEDIMIENTO LineTable_Reset()
PROCEDIMIENTO LineTable_Insert(lineNumber, idLinea)

// SÍMBOLOS
PROCEDIMIENTO Symbol_Iniciar()
PROCEDIMIENTO Symbol_Crear(tamInicial)
PROCEDIMIENTO Symbol_Ampliar(nuevoTamaño)
PROCEDIMIENTO Symbol_Reset()
PROCEDIMIENTO Symbol_Add(nombre$, tipo)

// DIAGNÓSTICOS
PROCEDIMIENTO Diag_Iniciar()
PROCEDIMIENTO Diag_Crear(tamInicial)
PROCEDIMIENTO Diag_Ampliar(nuevoTamaño)
PROCEDIMIENTO Diag_Reset()
PROCEDIMIENTO Diag_Add(msg$, linea, columna, fase$)

// SALIDA
PROCEDIMIENTO Output_Iniciar()
PROCEDIMIENTO Output_Crear(tamInicial)
PROCEDIMIENTO Output_Ampliar(nuevoTamaño)
PROCEDIMIENTO Output_Reset()
PROCEDIMIENTO Output_Add(linea$)

7.1 Tokens (implementacióncompleta en seudo‑código como ejemplo)

PROCEDIMIENTO Token_Iniciar()
    Estructura_Iniciar(TokenMax, TokenCount)
FIN

PROCEDIMIENTO Token_Crear(tamInicial)
    Estructura_Crear(TokenMax, TokenCount, tamInicial, tipo.Principal)
    DIM TokenType(TokenMax)
    DIM TokenLexema$(TokenMax)
    DIM TokenLine(TokenMax)
    DIM TokenColumn(TokenMax)
    DIM TokenLength(TokenMax)
FIN

PROCEDIMIENTO Token_Ampliar(nuevoTamaño)
    Estructura_AmpliarArray(TokenMax, TokenCount, nuevoTamaño)
    AmpliarArray(TokenType,    TokenMax, nuevoTamaño)
    AmpliarArray(TokenLexema$, TokenMax, nuevoTamaño)
    AmpliarArray(TokenLine,    TokenMax, nuevoTamaño)
    AmpliarArray(TokenColumn,  TokenMax, nuevoTamaño)
    AmpliarArray(TokenLength,  TokenMax, nuevoTamaño)
    TokenMax ← nuevoTamaño
FIN

PROCEDIMIENTO Token_Reset()
    SI TokenMax = 0 ENTONCES
        Token_Crear(50)
    FIN SI
    TokenCount ← 0
FIN

PROCEDIMIENTO Token_Add(tipo, lexema$, linea, columna, longitud)
    SI TokenMax = 0 ENTONCES
        Token_Crear(50)
    FIN SI
    SI TokenCount >= TokenMax ENTONCES
        Token_Ampliar(TokenMax + 50)
    FIN SI
    TokenCount ← TokenCount + 1
    TokenType(TokenCount)    ← tipo
    TokenLexema$(TokenCount) ← lexema$
    TokenLine(TokenCount)    ← linea
    TokenColumn(TokenCount)  ← columna
    TokenLength(TokenCount)  ← longitud
FIN

8. Siguientes pasos

Ya tenemos la base necesaria para portar todas las fases del transpilador al QL:

  • Lexer por líneas,
  • Parser por líneas,
  • Semántico con tabla de líneas y validaciones globales,
  • Transformaciones ZX→SuperBasic,
  • Generación del programa destino.

En la próxima entrada comenzaremos con el lexer por líneas, construyendo las estructuras definidas aquí.



ZB2SB. Paso 2. Director del proyecto

07:04 GMT (6 months ago)
Transpilador ZX BASIC — El Director del Proceso (Orquestador del Pipeline)

Índice de entradas del conversor



Introducción

Hasta ahora hemos desarrollado la gramática formal del ZX BASIC. El siguiente paso lógico es diseñar una pieza clave en cualquier compilador o transpilador: el Director del Proceso, responsable de coordinar lectura, análisis y generación. Dado que el objetivo final es ejecutar en un Sinclair QL real —un entorno con CPU más lenta y RAM muy limitada— adaptamos el diseño para que sea rápido y de mínimo consumo de memoria.


1. Motivación del Director del Proceso

Aunque cada fase del transpilador se puede ejecutar por separado, lo ideal es disponer de un módulo que:

  • Controle el orden correcto de ejecución.
  • Transmita la salida de cada fase a la siguiente.
  • Gestione errores y diagnósticos desde un único punto.
  • Permita activar/desactivar opciones (verbose, políticas del charset…).
  • Se adapte al destino: en nuestro caso, el QL, que requiere memoria mínima.

2. Pipeline completo del transpilador

FASE 01 — DEFINICIÓN PRECISA DEL LENGUAJE
FASE 02 — DIRECTOR DEL PROCESO
FASE 03 — LÉXICO (TOKENIZER)
FASE 04 — SINTÁCTICO (PARSER LL(1))
FASE 05 — ANÁLISIS SEMÁNTICO
FASE 06 — TRANSFORMACIONES (ZX BASIC → SuperBasic)
FASE 07 — GENERACIÓN DE CÓDIGO SuperBasic
FASE 08 — OPTIMIZACIÓN
FASE 09 — SALIDA + TESTS
FASE 10 — EMPAQUETADO Y HERRAMIENTAS
FASE 11 — DOCUMENTACIÓN
FASE 12 — FUTURO (ZX80/81, +2/+3…)

El Director no pertenece a ninguna de estas fases: las coordina a todas.


3. Las técnicas de lexing/parsing

Cuando procesamos el fichero de origen existen varias maneras de enfocar el compilador o transpilador. En algunos lenguajes el parser debe mirar hacia adelante o hacia atrás entre muchos tokens, lo que implica disponer de todos ellos; en otros, como ZX BASIC, cada línea constituye una unidad completa (“LineNumber → sentencias separadas por :”), lo que permite estrategias mucho más ligeras.

3.1 Tokenizar todo el fichero: LexAll + ParseProgram

  • Cómo funciona: el lexer produce la lista completa de tokens del fichero entero (incluye EOL por línea). El parser recorre esa lista y construye el AST global.
  • Ventajas: diseño simple; el parser recorre una secuencia homogénea; lookahead sencillo.
  • Inconvenientes: consume mucha RAM; listas grandes; GC costoso; hay que esperar a tener todos los tokens para empezar.

Variantes de “LexAll”

  • Tokenizar en memoria: todos los tokens en una lista → muy rápido en PC; consume mucha RAM en máquinas limitadas.
  • Tokenizar en disco: los tokens se guardan en un fichero intermedio → útil en PC; muy lento en microdrives del QL.

3.2 Tokenizar y parsear por líneas (técnica elegida)

  • Cómo funciona: el Director lee cada línea; el lexer tokeniza esa línea; el parser procesa sentencias separadas por :; libera tokens y pasa a la siguiente línea.
  • Ventajas: memoria mínima; velocidad muy alta en QL; una sola pasada; no requiere listas globales ni ficheros auxiliares.
  • Inconvenientes: el parser trabaja por línea (no por programa). Lenguajes con sentencias multilínea no podrían usar este método, pero en ZX BASIC cada línea es una unidad completa, así que encaja perfectamente.

Como el objetivo final es ejecutar en QL real, usaremos la técnica por líneas. El lexer expone LexLine(texto, numLinea) y el parser expone ParseLine(tokens, numLinea). Esta técnica maximiza la velocidad y minimiza el uso de memoria.

Comparativa rápida

Método Memoria Velocidad E/S Complejidad Cuándo usar
Tokenizar en memoria Alta Muy alta Mínima Baja PC con mucha RAM
Tokenizar en disco Muy baja Media (dos pasadas) Alta (fichero intermedio) Baja PC con disco rápido
Tokenizar por líneas Muy baja Alta Mínima Media QL o entornos con recursos limitados

4. Flujo general del proceso (optimizado QL)

Inicio
   ↓  leer fichero .BAS línea a línea
Lexer (por línea)
   ↓  tokens de esa línea (LineNumber, ... , ":" para sentencias)
Parser (por línea)
   ↓  procesa sentencias separadas por ":" y libera tokens
Transformaciones / Emisión (fases posteriores)

Esta secuencia garantiza un procesamiento ordenado, con uso mínimo de memoria y arranque inmediato desde la primera línea.


5. Esqueleto del Director (seudocódigo)

FUNCIÓN TranspilerDriver(rutaEntrada)
    Lexer.Preparar()
    Parser.Preparar()
    Semantico.Preparar()
    Transformador.Preparar()
    Emisor.Preparar()

    numeroLineaFisica ← 0
    PARA CADA lineaTexto EN leerLineas(rutaEntrada)
        numeroLineaFisica ← numeroLineaFisica + 1

        ListaTokens ← Lexer.Procesar(lineaTexto, numeroLineaFisica)
        AstLinea ← Parser.Procesar(ListaTokens, numeroLineaFisica)
        Semantico.RegistrarLinea(AstLinea)
        Semantico.VerificarReferenciaSaltos(AstLinea)
        AstTransformada ← Transformador.Aplicar(AstLinea, Politicas)
        Emisor.Emitir(AstTransformada)
    FIN PARA

    Semantico.VerificarSaltosPendientes()
    escribirConsola("Transpilación completada correctamente.")
FIN TranspilerDriver

6. Integración con las siguientes fases

  • Fase 3 (Semántico): se aplicará línea a línea o al final de un primer pase completo.
  • Fase 4 (Transformaciones ZX→SB): mapeo del charset y normalizaciones.
  • Fase 5 (Generación SuperBasic): emisión del código destino.
  • Fase 6 (Optimización): ajustes sobre el código resultante.

7. Próximo paso

En la siguiente entrada comenzaremos con el lexer (análisis léxico), usando la EBNF final y la estructura LineNumber → SimpleStatement { ":" SimpleStatement }.

En el modo verbose se mostrarán por pantalla todos los tokens emitidos por el lexer, lo que es útil para diagnósticos y muy informativo del proceso generado.


APÉNDICE: Director del proceso completo

Por completar, aquí añado el seudocódigo del director del proyecto más desarrollado, incluyendo control de errores y manejo de la opción verbose para salida de información adicional en pantalla:

// ==================================================================================
//  Director del Proceso (QL) — Seudo‑código con gestión de errores
//  • Procesamiento por líneas (memoria mínima)
//  • Manejo explícito de diagnósticos en TODAS las fases
//  • Política de error configurable: "AbortarAlPrimerError" o "AcumularYContinuar"
//  • Opción Verbose con salida en pantalla de información adicional
// ==================================================================================

TIPO PoliticaErrores = { AbortarAlPrimerError, AcumularYContinuar }

FUNCIÓN TranspilerDriver.Ejecutar(rutaEntrada, verbose, politicaErrores) → Booleano
    SI noExisteArchivo(rutaEntrada) ENTONCES
        escribirConsola("Error: no se encuentra el fichero de entrada.")
        RETORNAR FALSO
    FIN SI

    SI NO Lexer.Preparar(verbose) ENTONCES
        ReportarDiagnosticos("Lexer.Preparar", Lexer.Diagnostics)
        RETORNAR FALSO
    FIN SI
    SI NO Parser.Preparar(verbose) ENTONCES
        ReportarDiagnosticos("Parser.Preparar", Parser.Diagnostics)
        RETORNAR FALSO
    FIN SI
    SI NO Semantico.Preparar(verbose) ENTONCES
        ReportarDiagnosticos("Semantico.Preparar", Semantico.Diagnostics)
        RETORNAR FALSO
    FIN SI
    SI NO Transformador.Preparar(verbose) ENTONCES
        ReportarDiagnosticos("Transformador.Preparar", Transformador.Diagnostics)
        RETORNAR FALSO
    FIN SI
    SI NO Emisor.Preparar(verbose) ENTONCES
        ReportarDiagnosticos("Emisor.Preparar", Emisor.Diagnostics)
        RETORNAR FALSO
    FIN SI

    numeroLineaFisica ← 0
    huboErrores ← FALSO

    PARA CADA lineaTexto EN leerLineas(rutaEntrada)
        numeroLineaFisica ← numeroLineaFisica + 1

        INTENTAR
            ListaTokens ← Lexer.Procesar(lineaTexto, numeroLineaFisica)
        CAPTURAR ex
            Lexer.Diagnostics.Agregar(DiagFatal(ex, numeroLineaFisica))
        FIN

        SI noEstaVacio(Lexer.Diagnostics) ENTONCES
            ReportarDiagnosticos("Lexer (línea " + aCadena(numeroLineaFisica) + ")", Lexer.Diagnostics)
            huboErrores ← VERDADERO
            SI politicaErrores = AbortarAlPrimerError ENTONCES RETORNAR FALSO FIN SI
            CONTINUAR PARA
        FIN SI

        INTENTAR
            AstLinea ← Parser.Procesar(ListaTokens, numeroLineaFisica)
        CAPTURAR ex
            Parser.Diagnostics.Agregar(DiagFatal(ex, numeroLineaFisica))
        FIN

        SI noEstaVacio(Parser.Diagnostics) ENTONCES
            ReportarDiagnosticos("Parser (línea " + aCadena(numeroLineaFisica) + ")", Parser.Diagnostics)
            huboErrores ← VERDADERO
            SI politicaErrores = AbortarAlPrimerError ENTONCES RETORNAR FALSO FIN SI
            CONTINUAR PARA
        FIN SI

        INTENTAR
            Semantico.RegistrarLinea(AstLinea)
            Semantico.VerificarReferenciaSaltosParcial(AstLinea)
        CAPTURAR ex
            Semantico.Diagnostics.Agregar(DiagFatal(ex, numeroLineaFisica))
        FIN

        SI noEstaVacio(Semantico.Diagnostics) ENTONCES
            ReportarDiagnosticos("Semántico (línea " + aCadena(numeroLineaFisica) + ")", Semantico.Diagnostics)
            huboErrores ← VERDADERO
            SI politicaErrores = AbortarAlPrimerError ENTONCES RETORNAR FALSO FIN SI
            CONTINUAR PARA
        FIN SI

        INTENTAR
            AstTransformada ← Transformador.Aplicar(AstLinea, Politicas)
        CAPTURAR ex
            Transformador.Diagnostics.Agregar(DiagFatal(ex, numeroLineaFisica))
        FIN

        SI noEstaVacio(Transformador.Diagnostics) ENTONCES
            ReportarDiagnosticos("Transformador (línea " + aCadena(numeroLineaFisica) + ")", Transformador.Diagnostics)
            huboErrores ← VERDADERO
            SI politicaErrores = AbortarAlPrimerError ENTONCES RETORNAR FALSO FIN SI
            CONTINUAR PARA
        FIN SI

        INTENTAR
            Emisor.Emitir(AstTransformada)
        CAPTURAR ex
            Emisor.Diagnostics.Agregar(DiagFatal(ex, numeroLineaFisica))
        FIN

        SI noEstaVacio(Emisor.Diagnostics) ENTONCES
            ReportarDiagnosticos("Emisor (línea " + aCadena(numeroLineaFisica) + ")", Emisor.Diagnostics)
            huboErrores ← VERDADERO
            SI politicaErrores = AbortarAlPrimerError ENTONCES RETORNAR FALSO FIN SI
            CONTINUAR PARA
        FIN SI
    FIN PARA

    INTENTAR
        Semantico.VerificarSaltosPendientes()
        Semantico.VerificarBloquesPendientes()
    CAPTURAR ex
        Semantico.Diagnostics.Agregar(DiagFatal(ex, 0))
    FIN

    SI noEstaVacio(Semantico.Diagnostics) ENTONCES
        ReportarDiagnosticos("Semántico (verificación final)", Semantico.Diagnostics)
        huboErrores ← VERDADERO
        SI politicaErrores = AbortarAlPrimerError ENTONCES RETORNAR FALSO FIN SI
    FIN SI

    SI huboErrores ENTONCES
        escribirConsola("Transpilación finalizada con incidencias.")
        RETORNAR FALSO
    SINO
        escribirConsola("Transpilación completada correctamente.")
        RETORNAR VERDADERO
    FIN SI
FIN FUNCIÓN

PROCEDIMIENTO ReportarDiagnosticos(etapa, listaDiag)
    escribirConsola("=== Errores en " + etapa + " ===")
    PARA CADA diag EN listaDiag
        escribirConsola(diag.aTexto())
    FIN PARA
FIN PROCEDIMIENTO

FUNCIÓN DiagFatal(excepcion, linea) → Diagnostico
    diag ← nuevo Diagnostico
    diag.Linea ← linea
    diag.Columna ← 1
    diag.Mensaje ← "Fatal: " + excepcion.mensaje
    RETORNAR diag
FIN FUNCIÓN

Thursday, 12. March 2026

ZB2SB. Paso 1. Definir el Lenguaje de origen

12:49 GMT (6 months ago)
Gramática ZX BASIC Spectrum 48K — BNF completa y explicada

Índice de entradas del conversor

Modificado el 12/03/2026, ampliaciones en color rojo



Gramática del ZX BASIC del Spectrum 48K

Esta entrada ofrece un resumen estructurado de la gramática del ZX BASIC del Sinclair ZX Spectrum 48K, basado en las referencias originales del manual oficial del Spectrum y las versiones digitalizadas del manual en español. Primero se presenta una versión ligera y, después, la definición formal completa.

  • BNF/EBNF práctica, apta para implementar (recursive descent o LL(1) con pequeñas adaptaciones).
  • Las restricciones del hardware (p. ej. rango de números de línea) se indican en comentarios.
  • Se prioriza claridad en la introducción; los detalles formales aparecen en el BNF completo final.

NOTA IMPORTANTE: heredado de los ZX80/81 con solo 1 KB disponible, para ahorrar memoria todas las palabras clave se almacenan internamente como tokens de 1 byte. Por tanto, toda sentencia debe comenzar obligatoriamente por un token de palabra clave. No puede comenzar por un identificador. LET es obligatorio siempre. REM solo se reconoce si es exactamente REM.

Estos ajustes son mínimos pero críticos para definir correctamente la gramática y la generación de tokens.


1. Estructura de un programa

Un programa en ZX BASIC está compuesto por líneas numeradas:

<linea> ::= <numero> <espacio> <instruccion>
  • Los números de línea van de 1 a 9999.
  • Una línea puede contener una o varias instrucciones separadas por :.

2. Tipos de datos

2.1 Tipos básicos

  • Números: coma flotante de 40 bits.
  • Cadenas: delimitadas por comillas ("texto").
  • No existen enteros ni booleanos como tipos nativos; la lógica usa 0 para falso y <>0 para cierto.

2.2 Nombres de variables

<var_num> ::= <letra> [ <letra_o_digito> ]
<var_str> ::= <letra> [ <letra_o_digito> ] "$"
<var_array> ::= <var> "(" <lista_expresiones> ")"
  • Las variables numéricas y de cadena con el mismo nombre son distintas.
  • Las matrices pueden ser numéricas o de cadena.

3. Expresiones

3.1 Expresiones numéricas

Precedencia de operadores (de mayor a menor):

  1. Unario: -
  2. Potencia: ^
  3. Multiplicación / División: * /
  4. Suma / Resta: + -
  5. Relacionales: =, <, >, <=, >=, <>
  6. Lógicos: AND, OR, NOT

Fuentes: capítulos 3, 7 y 13 del manual.

3.2 Expresiones de cadena

  • Concatenación: "A" + "B".
  • Slicing: A$(n TO m).
  • Longitud: LEN A$.

Fuente: capítulo 8 del manual.


4. Sentencias

4.1 Asignación

<asignacion> ::= LET <var> = <expresion>

LET es obligatorio en el Spectrum 48K (no puede omitirse).

4.2 Entrada y salida

  • PRINT
  • INPUT
  • CLS
  • TAB(n), AT y,x

Fuente: capítulos 2 y 15 del manual.


5. Control de flujo

5.1 IF

IF <expresión> THEN <instrucciones>

Ejecuta todas las instrucciones (separadas por :) hasta fin de línea. No existe ELSE en el ZX Spectrum 48K.

5.2 Bucles FOR

FOR <var> = <expr> TO <expr> [STEP <expr>]
...
NEXT <var>

Fuente: capítulo 4 del manual.

5.3 Subrutinas

GOSUB número
RETURN

Fuente: capítulo 5 del manual.

5.4 Saltos

  • GO TO
  • STOP
  • CONTINUE

6. Datos

READ <lista_variables>
DATA <lista_constantes>
RESTORE [número]

Fuente: capítulo 6 del manual.


7. Comentarios

En ZX Spectrum 48K, la sentencia REM admite exactamente dos formas:

  • Comentario vacío: REM seguido inmediatamente de fin de línea.
  • Comentario con contenido: REM seguido de un ESPACIO y, a continuación, cualquier número de caracteres (incluidos espacios) hasta fin de línea.

Ejemplos válidos:

10 REM
20 REM Hola mundo
30 REM  Más espacios al inicio del comentario

Ejemplos inválidos:

40 REMHola     ; falta el espacio tras REM
50 REMHola ; tabulación inmediata tras REM (no permitido)

EBNF:

ESPACIO      = " ";
EOL          = "\n";
anyCharacter = ? cualquier carácter Unicode ? ;

remStmt =
      "REM"
    | "REM" , ESPACIO , { anyCharacter } ;

8. Funciones incorporadas

Matemáticas

ABS, INT, SGN, SIN, COS, TAN, ASN, ACS, ATN, SQR, EXP, LN, PI …

Cadenas

LEN, STR$, VAL, CHR$, CODE

Sistema

PEEK, POKE, USR, IN, OUT, RND, RANDOMIZE

Fuente: capítulos 9, 10, 11, 14 y 23 del manual.


9. Arrays

DIM A(10)
DIM N$(5)
  • Índices desde 1 hasta el tamaño declarado (arrays base 1).
  • Acceso: A(1), A(5), N$(1)…

10. Gráficos

  • PLOT x,y
  • DRAW dx,dy
  • CIRCLE x,y,r
  • POINT x,y

Fuente: capítulo 17 del manual.


11. Colores

  • INK n
  • PAPER n
  • FLASH n
  • BRIGHT n
  • INVERSE n
  • OVER n
  • BORDER n

Fuente: capítulo 16 del manual.


12. Sonido

BEEP duracion, tono

Fuente: capítulo 19 del manual.


13. Cinta y ficheros

  • SAVE
  • LOAD
  • MERGE
  • VERIFY

Fuente: capítulo 20 del manual.


14. Gramática mínima para transpiladores

<program> ::= { <line> }

<line> ::= <number> <Statement>

<Statement> ::= <SimpleStatement> { ":" <SimpleStatement> }

<SimpleStatement> ::= <assign>
                     | PRINT <print_list>
                     | INPUT <var_list>
                     | IF <expr> THEN <Statement>
                     | FOR <assign> TO <expr> ["STEP" <expr>]
                     | NEXT <var>
                     | GOSUB <number>
                     | RETURN
                     | GO TO <number>
                     | READ <var_list>
                     | DATA <const_list>
                     | DIM <dim_list>
                     | PLOT | DRAW | CIRCLE | POINT | ...

15. BNF/EBNF del ZX BASIC del Spectrum 48K (versión corregida)

1) Léxico

letter        = "A".."Z" | "a".."z"
digit         = "0".."9"
nonZeroDigit  = "1".."9"
alnum         = letter | digit

numVar        = letter , [ alnum ]
strVar        = letter , [ alnum ] , "$"
var           = numVar | strVar

intLiteral    = digit , { digit }
fractPart     = "." , { digit }
expPart       = ("E" | "e") , [ "+" | "-" ] , digit , { digit }

numLiteral    = intLiteral , [ fractPart ] , [ expPart ]
              | fractPart , [ expPart ]

ESPACIO      = " "
EOL          = "\n"
anyCharacter = ? cualquier carácter Unicode ?

strLiteral    = '"' , { anyCharacter } , '"'


; REM solo se reconoce como palabra clave si el token es exactamente "REM".
; Un identificador NO puede iniciar una sentencia.

2) Estructura del programa

program       = { line }

line          = lineNumber , Statement
lineNumber    = LineNumberToken
LineNumberToken = nonZeroDigit , [ digit ] , [ digit ] , [ digit ]
(* Rango efectivo: 1..9999; token específico generado por el lexer *)

3) Statement y SimpleStatement

Statement     = SimpleStatement , { ":" , SimpleStatement }

; SimpleStatement debe comenzar por token de palabra reservada

SimpleStatement =
                   letStmt
                 | printStmt
                 | inputStmt
                 | ifStmt
                 | forStmt
                 | nextStmt
                 | gotoStmt
                 | gosubStmt
                 | returnStmt
                 | stopStmt
                 | continueStmt
                 | readStmt
                 | dataStmt
                 | restoreStmt
                 | dimStmt
                 | clsStmt
                 | graphicStmt
                 | colorStmt
                 | soundStmt
                 | tapeStmt
                 | remStmt

4) Expresiones

expr          = logicOr
logicOr       = logicAnd , { "OR" , logicAnd }
logicAnd      = relation , { "AND" , relation }

relation      = arithExpr , [ relOp , arithExpr ]
relOp         = "=" | "<>" | "<" | ">" | "<=" | ">="

arithExpr     = term , { ("+" | "-") , term }
term          = power , { ("*" | "/") , power }
power         = unary , { "^" , unary }

unary         = [ "-" | "NOT" ] , primary

primary       = numLiteral
              | strLiteral
              | varRef
              | funcCall
              | "(" , expr , ")"

5) Variables y arrays (base 1)

varRef        = arrayRef | var

arrayRef      = ( numVar | strVar ) ,
                "(" , indexExpr , { "," , indexExpr } , ")"

indexExpr     = expr

6) Funciones

funcCall      = funcNum | funcStr | sysFunc

funcNum       = ("ABS" | "INT" | "SGN" | "SIN" | "COS" | "TAN"
                | "ASN" | "ACS" | "ATN" | "SQR" | "EXP" | "LN"
                | "VAL" | "PI")
                "(" , [ expr ] , ")"

funcStr       = "LEN" "(" , ( strVar | strLiteral ) , ")"
              | "STR$" "(" , expr , ")"
              | "CHR$" "(" , expr , ")"

sysFunc       = "PEEK" "(" , expr , ")"
              | "USR"  "(" , expr , ")"
              | "CODE" "(" , (strVar | strLiteral) , ")"

7) Sentencias (detalle)

remStmt   = "REM"
          | "REM" , ESPACIO , { anyCharacter }
 
letStmt   = "LET" , ( varRef | var ) , "=" , expr
printStmt = "PRINT" , [ printList ]
printList = printItem , { ("," | ";") , printItem }
printItem = expr
          | "TAB" "(" , expr , ")"
          | "AT" expr "," expr

inputStmt = "INPUT" , varList
varList   = varRef , { "," , varRef }

ifStmt    = "IF" , expr , "THEN" , Statement

forStmt   = "FOR" , numVar , "=" , expr , "TO" , expr , [ "STEP" , expr ]
nextStmt  = "NEXT" , [ numVar ]

gotoStmt  = ("GOTO" | "GO TO") , lineNumber
gosubStmt = ("GOSUB" | "GO SUB") , lineNumber
returnStmt= "RETURN"

stopStmt  = "STOP"
continueStmt = "CONTINUE"

readStmt  = "READ" , varList
dataStmt  = "DATA" , constList
constList = constItem , { "," , constItem }
constItem = numLiteral | strLiteral

restoreStmt = "RESTORE" , [ lineNumber ]

dimStmt   = "DIM" , dimList
dimList   = dimItem , { "," , dimItem }
dimItem   = ( numVar | strVar ) , "(" , sizeExpr , { "," , sizeExpr } , ")"
sizeExpr  = expr

clsStmt   = "CLS"

graphicStmt = plotStmt | drawStmt | circleStmt | pointStmt
plotStmt  = "PLOT" , expr , "," , expr
drawStmt  = "DRAW" , expr , "," , expr
circleStmt= "CIRCLE" , expr , "," , expr , "," , expr
pointStmt = "POINT" , expr , "," , expr

colorStmt = ("INK" | "PAPER" | "FLASH" | "BRIGHT" | "INVERSE" | "OVER" | "BORDER") , expr

soundStmt = "BEEP" , expr , "," , expr

tapeStmt  = ("SAVE" | "LOAD" | "VERIFY" | "MERGE") , strLiteral

16. Referencias

Wednesday, 11. March 2026

ZB2SB. Conversor de Basic de los ZX al Super Basic del QL (Reinicio)

22:30 GMT (6 months ago)

Índice de entradas del conversor


📝 Diseño de un Transpilador ZX Spectrum BASIC → QL SuperBASIC

Reinicio del proyecto (esta vez espero acabarlo…)

Retomo este proyecto con la intención firme de completarlo. Siempre he querido disponer de una herramienta capaz de traducir programas escritos en lenguaje ZX Basic del Spectrum al lenguaje SuperBASIC del Sinclair QL. Aunque ya he empezado varias veces, esta vez quiero estructurarlo bien para poder llegar al final. La idea es seguir una serie de pasos claros, desde la definición del alcance hasta la ejecución de pruebas reales.

Aunque ambos dialectos BASIC comparten muchos conceptos, tienen diferencias importantes que obligan a realizar transformaciones no triviales. En esta entrada describo la arquitectura general de un transpilador, enfocada a este caso concreto.


1) Definir el alcance y las variantes

✔ Lenguaje de origen

ZX BASIC (Spectrum):

  • Líneas numeradas
  • GOTO, GOSUB, RETURN
  • LET, RANDOMIZE, PLOT, INK, PAPER
  • Tokens gráficos
  • PEEK/POKE, USR
  • Comentarios con REM o '

✔ Lenguaje destino

Sinclair QL SuperBASIC:

  • No requiere números de línea
  • Bloques IF…END IF, REPeat, SELect ON
  • Procedimientos (PROCedure) y funciones (DEFine FuNction)
  • I/O con canales (OPEN, PRINT #ch)
  • Gráficos con MODE, LINE, POINT

✔ Compatibilidad y variantes

  • Compatibilidad con ZX80/81 BASIC
  • Futuro soporte para Spectrum 128/+2/+3
  • Ajuste de coordenadas y resoluciones

✔ Cobertura funcional

Cobertura mínima:

  • Asignaciones y expresiones
  • Estructuras: IF, FOR, WHILE, GOTO, GOSUB
  • Arrays
  • PRINT / INPUT
  • Matemáticas básicas

Cobertura ampliada:

  • Gráficos y sonido
  • MERGE, LOAD, SAVE
  • ON ERROR, ON x GOSUB
  • Manejo de cadenas
  • Temporización / canales I/O

No soportado:

  • PEEK, POKE, USR

2) Reunir / normalizar las gramáticas

  • Gramática del ZX BASIC: keywords, literales, tokens gráficos, comentarios.
  • Gramática del SuperBASIC usada para validar la salida.

3) Arquitectura general del transpilador

  1. Lexer: obtener tokens.
  2. Parser + AST: nodos Program, If, For, Print, etc.
  3. Análisis semántico:
    • Tabla de símbolos
    • Detección de subrutinas
    • Control de flujo (CFG)
    • Tipos y arrays
  4. Transformación del AST:
    • Eliminar números de línea
    • Reescribir saltos
    • GOSUB → PROCEDURE
    • Normalización I/O
    • Conversión gráfica
  5. Generación de código SuperBASIC
  6. Post‑procesado

4) Reglas de mapeo

4.1 Estructura y control de flujo

  • IF…THEN…ELSEIF…END IF
  • WHILE/WENDREPeat + EXIT WHEN
  • GOTO → intentar estructurar
  • GOSUBPROCedure

4.2 Variables y arrays

  • BASIC Spectrum: base 1 → QL: base 0
  • Cadenas $
  • Ajuste de rangos en DIM

4.3 Entrada/Salida

  • PRINT / PRINT #ch
  • INPUT / INPUT #ch
  • Uso de ; y ,

4.4 Funciones

  • SIN, COS, ABS, RND
  • RANDOMIZE
  • VAL, STR$, LEFT$, RIGHT$

4.5 Gráficos y sonido

  • Spectrum: PLOT, DRAW, CIRCLE
  • QL: POINT, LINE, CIRCLE
  • Atributos: sin equivalente

4.6 Memoria

  • PEEK, POKE, USR → no portables

4.7 Errores

  • ON ERROR → comportamiento diferente

5) Estrategia para convertir GOTO/GOSUB

  • Construir un CFG
  • Buscar patrones:
    • If‑then → IF…END IF
    • Saltos hacia atrás → loops
    • Subrutinas → PROCedure
  • Modo compatibilidad si no es seguro

6) Conjunto mínimo de casos de prueba

  • Asignaciones
    10 LET a=1: LET b=a+2: PRINT a+b
  • IF simple
    10 IF a>10 THEN PRINT "OK"
  • IF con ELSE
    10 IF a>10 THEN
    20 PRINT "OK"
    30 ELSE PRINT "NO"
    40 END IF
  • FOR/NEXT
    10 FOR i=1 TO 10: PRINT i: NEXT i
  • WHILE/WEND
    10 LET i=0
    20 WHILE i
  • GOSUB/RETURN
    10 LET x=5: GOSUB 90: PRINT x: STOP
    90 LET x=x*2: RETURN
  • Arrays y cadenas
    10 DIM a(10): FOR i=1 TO 10: LET a(i)=i*i: NEXT i: PRINT a(5)
    20 LET s$="HELLO": PRINT LEN s$, RIGHT$(s$,2)
  • Gráficos
    10 PLOT 10,10: DRAW 20,0: CIRCLE 30,30,10

7) Ejemplo de traducción básica

Entrada ZX Spectrum BASIC:

10 REM Calculo de suma
20 LET s=0
30 FOR i=1 TO 10
40 LET s=s+i
50 NEXT i
60 IF s>50 THEN PRINT "MAYOR"; ELSE PRINT "MENOR O IGUAL";
65 PRINT " DE 50"
70 GOSUB 90
80 PRINT "FIN": STOP
90 PRINT "TOTAL=";s: RETURN

Salida QL SuperBASIC:

REM Calculo de suma
s = 0
FOR i = 1 TO 10
  s = s + i
END FOR
IF s > 50 THEN
  PRINT "MAYOR";
ELSE
  PRINT "MENOR O IGUAL";
END IF
PRINT " DE 50"
PROC_show_total(s)
PRINT "FIN"
STOP

PROCedure PROC_show_total(total)
  PRINT "TOTAL="; total
END PROCedure

8) Ejemplo de posible mapeo de gráficos

Spectrum:

10 INK 2: PAPER 0: CLS
20 PLOT 10,10: DRAW 50,0
30 CIRCLE 40,40,10

QL:

INK 2 : PAPER 0 : CLS
POINT 10,10
LINE 10,10 TO 60,10
CIRCLE 40,40,10

9) Ejemplo de traducción (no trivial)

10 REM Serie, demo graficos y subrutina
20 LET s=0: LET x=64: LET y=64
30 FOR i=1 TO 5
40 LET s=s+i
50 NEXT i
60 IF s>10 THEN PRINT "MAYOR";: PRINT " "; s ELSE PRINT "NO";: PRINT " "; s
70 PLOT 10,10: DRAW 20,0: DRAW 0,20: DRAW -20,0: DRAW 0,-20
80 GOSUB 200
90 DATA 3,5,8
100 READ a,b,c
110 PRINT "DATA:"; a; ","; b; ","; c
120 STOP
200 REM duplica s
210 LET s=s*2
220 RETURN

Salida QL:

100 REMark Calculo de suma
110 REMark Serie, demo graficos y subrutina
120 CLS
130 s=0
140 x=64
150 y = 64
160 FOR i = 1 TO 5
170 s = s + i
180 END FOR i
190 IF s > 10 THEN
200   PRINT "MAYOR";
210   PRINT " "; s
220 ELSE
230   PRINT "NO";
240   PRINT " "; s
250 END IF

270 REMark Gráficos: PLOT y DRAW relativo
280 INK 7 : PAPER 0
290 __gx = 10 : __gy = 10
300 POINT __gx, __gy
310 LINE __gx, __gy TO __gx + 20, __gy
320 LINE __gx, __gy TO __gx, __gy + 20
330 LINE __gx, __gy TO __gx - 20, __gy
340 LINE __gx, __gy TO __gx, __gy - 20

350 PROC_duplica_s(s)

370 _DATA_INIT
390 FOR i=1 TO 3
400   PRINT _READ
410 END FOR i

450 DEFine PROCedure PROC_duplica_s(by_s)
460   s = s * 2
470 END DEFine

490 DEFine PROCedure _DATA_INIT
500   Data_Init = 0
510 END DEFine

530 DEFine FuNction _READ
540   IF Data_Init <> 1 THEN _DATA
560   __DATA(0) = __DATA(0) + 1
570   RETurn __DATA(__DATA(0))
580 END DEFine _READ

600 DEFine PROCedure _DATA
620   Data_Init = 1
630   DIM __DATA(3)
640   __DATA(0) = 0
650   __DATA(1) = 3
660   __DATA(2) = 5
670   __DATA(3) = 8
680 END DEFine

Wednesday, 11. February 2026

Saturday, 03. January 2026

Q-emuLator 4.0 for Windows

04:23 GMT (8 months ago)

Version 4.0 of Q-emuLator has been released for Windows 7 SP1 and later.


Extensive research went into making the CPU emulation cycle-accurate (also simulating its interaction with the ZX8301 video ULA and the 8049 coprocessor), creating a new lower-level format for microdrive images and studying the hardware of the ICL One Per Desk computer (a cousin of the QL).

Q-emuLator 4 also adds emulation of the QSound interface (3 sound channels), can run the pre-existing extended graphics modes inside a window, includes an improved ram disk driver and has more extensive debugging capabilities

For the nostalgic, there are monochrome monitor colour options and simulation of the microdrive motor and tape noise.

The overall experience of running the emulator should be smoother, with new features like default directories for ROMs/software/configurations, reduced audio latency, a Pause command (Windows-Alt-P), auto CPU speed setting, ability to create new empty MDV, MDVRAW, QXL.WIN and IMG file containers.

The manual has been updated and expanded and there are now an online troubleshooting page and a 'check for updates' command.

For a more detailed list of everything that's new in version 4, see this web page.

Monday, 08. December 2025

QL desconocido: Sampled Sound System

20:48 GMT (9 months ago)

El sonido del QL de caja es francamente limitado. El beeper es todavía peor que el del Spectrum, y más difícil de programar incluso en BASIC.

Por ello, a pesar de las limitadas ventas del Sinclair QL y de lo calamitoso de su lanzamiento, varias pequeñas empresas de hardware diseñaron y fabricaron periféricos y tarjetas de ampliación para potenciar las capacidades sonoras del QL.

En este post, revisamos la tarjeta QSound de ABC Elektronic y en este otro, la interfaz QL MIDI de Miracle Systems. Pero hubo más soluciones como el QV200 Speech Synthesizer o el QTalk, aunque estas dos se enfocaban en la síntesis de voz más que en la generación y reproducción de sonido.

Tras la muerte comercial del QL en 1986, tras la compra de la IP de Sinclair por parte de Amstrad, El desarrollo de hardware compatible con el QL continuó gracias  empresas como CST (con la línea de ordenadores Thor), Sandy (con el QXT-640 y el fallido Futura) y Miracle Systems (con las aceleradoras Gold y SuperGold Card, y la tarjeta QXL para PC).

Pero fue Peter Graf en 1998 quien llevó el “sistema QL” a la máxima expresión, con el diseño del Q40 y el Q60. Estás máquinas compatibles QL fueron fabricadas y comercializadas primero por QBranch y después por D&D Systems, aunque sólo se fabricaron unas pocas decenas de unidades.

Estos sistemas estaban basados en una placa formato AT de nuevo diseño, con memoria RAM y tarjetas de expansión siguiendo los estándares del mercado de PCs de la época.

El Q60 permitía hasta 4Mb de RAM ejecutando QDOSClassic y hasta 32Mb ejecutando SMSQ/E, y la adopción de tarjetas ISA permitía el uso de controladoras de disco duro IDE, floppy HD y red Ethernet. Asimismo, podía utilizar los modos de video de alta resolución de los Hi-colour drivers de SMSQ/E llegando hasta 1024×512 en color de 32 bits.

La especificación del Q60 en su máxima configuración (según q40.de) era:

  • Q60/80: 68LC060 CPU, 80 MHz, MMU
  • 68060 superscalar architecture, dual execution units
  • Up to 160 BogoMIPS performance for QDOS+SMSQ/E
  • 4 to 128 MB RAM, PS/2 module sockets
  • 256 to 1024 kB ROM
  • Highspeed 32 bit graphics, with original QL modes
  • 65536 colours at 1024 x 512 pixel resolution
  • Multisync monitor output
  • PC Keyboard interface (DIN)
  • 20 kHz Stereo sound
  • Battery buffered clock, 2 KB nonvolatile RAM
  • Controller for 2 IDE harddisks or CD-ROM
  • 2 Serial ports with 115200 Baud, Parallel port, Joystick port (on IO card supplied with mainboard)
  • Hardware extension slot supports ISA cards
  • Fits directly into Minitower or other standard case
  • +5V / +12V power supply

Como se ve en la especificación, una característica destacada es el sonido estéreo a 20KHz.

En octubre del año 2000, Simon N. Goodwin publicó la última versión de sus drivers de sonido para QDOS, compatibles con QDOSClassic tanto en Amiga como en el Q40/Q60 y con SMSQ/E en el Q40/Q60. Este software está disponible en la QL Homepage (qlsss).

Los emuladores Q-emuLator y QPC2 soportan el QSSS (QL Sampled Sound System), por lo que se puede ver como funciona sin necesidad de disponer de hardware compatible.

Cuando Peter Graf lanzó su tarjeta de ampliación por el puerto ROM QIMSI en 2024, decidió incluir además de almacenamiento microSD, puertos de teclado y mouse y un puerto serie de alta velocidad, hardware para reproducir sonido sampleado mono utilizando el altavoz del QL. Para poder utilizar esta característica, es necesario hacer un pequeño MOD para conectar el puerto ROM al altavoz interno. Con el software suministrado con el dispositivo incluyó un reproductor de archivos formato ub (unsigned byte) que son los que reproduce este sistema. También publicó una guía para convertir cualquier archivo de sonido al formato ub compatible usando audacity.

Pero lo suyo es ver si esto funciona en un QL real. Como tengo una QIMSI de cuando la reunión del 40 aniversario del QL, me decidí a hacer el MOD, convertir algunos MP3 a UB y hacer algunas pruebas. Para que el sonido sampleado suene bien se necesita como mínimo una Super Gold Card, con la Gold Card suena pero el QL va petadísimo. No pasa de ser una curiosidad, pero utilizar varios programas en multitarea mientras el QL reproduce Rain de The Cult tiene su punto…..

Para facilitar la reproducción de archivos de música, he programado un sencillísimo front-end en pointer environment que permite reproducir diferentes tipos de archivos de sonido.

Para quien tenga interés en ver cómo funciona, el zip enlazado más abajo contiene un setup para Q-emuLator. Incluye el programa en Pointer Environment que he hecho como frontend del reproductor, las extensiones necesarias y un archivo boot que lo carga todo. En el archivo readme están las instrucciones y las configuraciones de Q-emuLator que hay que utilizar. El ZIP incluye una guía en PDF para convertir archivos de sonido a formato _ub usando Audacity.

https://retrowiki.es/download/file.php?id=200041249

QL Forever!

Monday, 03. November 2025

Sinclair QL desconocido: QL y MIDI

21:51 GMT (10 months ago)

El protocolo MIDI (Musical Instruments Digital Interface), que fue publicado en 1983 gracias a una serie de acuerdos entre fabricantes de equipos e instrumentos musicales electrónicos, posibilitó establecer un estándar de comunicación entre éstos y que, por extensión, también se pudieran comunicar con ordenadores con interfaz MIDI.

La definición MIDI 1.0 de 1983 especificaba tanto la conexión física y el interfaz de hardware, como el formato de los datos y el orden y disposición de los mensajes que se pueden transmitir entre dispositivos. De hecho, la comunicación MIDI es muy parecida al protocolo RS-232, aunque los niveles eléctricos son diferentes y permite una mayor velocidad de transmisión.

Un fichero MIDI no contiene sonido digitalizado, como un archivo .wav o .mp3 (que además está comprimido), sino que contiene únicamente las instrucciones necesarias para que un dispositivo compatible genere los sonidos correspondientes. Estas instrucciones están en forma de mensajes MIDI que indican al sintetizador (entre otras cosas) qué sonidos de los que dispone debe utilizar, qué notas debe activar, y con qué intensidad debe hacerlo.

Una ventaja de trabajar con MIDI es la facilidad con la que esta música se puede editar y modificar, cambiando su tempo, la altura de algunas notas, los sonidos utilizados, etc.

Para conocer el protocolo MIDI, una obra muy recomendable en castellano es “Audio digital y MIDI”, de Sergi Jordà Puig, Anaya Multimedia, Madrid 1997 https://www.ccapitalia.net/reso/articulos/audiodigital/index.htm

La popularización de los microordenadores en la década de los 80 provocó que la utilización del protocolo MIDI se extendiera más allá de los estudios de grabación y los músicos profesionales. Se comercializaron interfaces MIDI para los micros más populares, y el Atari ST, que incorporaba puertos MIDI de serie, junto al software Cubase se convirtió en un estándar de facto para los estudios de grabación profesionales.

Poder utilizar el protocolo MIDI en los microordenadores personales abría un amplio abanico de posibilidades, más allá de la producción, edición y grabación de música. Algunos desarrolladores de software lúdico vieron la posibilidad de dotar a sus creaciones de música y sonido más allá de los que los chips PSG (como el AY-3-8910 o el SID) podían ofrecer. Conectando el ordenador a un sintetizador MIDI, las posibilidades de los juegos por lo que a música y sonido se refiere crecían de manera exponencial.

Precisamente el Roland MT-32 fue uno de los sintetizadores más utilizados en la época para conectar a los ordenadores con capacidades MIDI. Lanzado en 1987, el Roland MT-32 Multi-Timbre Sound Module se comercializó como un sintetizador externo económico para músicos aficionados con un precio de venta de 695 dólares. Gracias a sus módulos compatibles, fue uno de los primeros estándares de facto en la música por ordenador.

Mientras todo esto pasaba, el Sinclair QL empezaba a vivir su agonía comercial. Fue lanzado en 1984 con muchos problemas de calidad y retrasos en las entregas y, desde 1985, compartía segmento de  mercado con el Atari ST y el Commodore Amiga. La compra de la IP de Sinclair por parte de Amstrad en abril de 1986 y la decisión de Alan Sugar de discontinuar la línea de producto QL para continuar únicamente produciendo evoluciones del ZX Spectrum convirtieron al QL en un producto residual, cuyo mercado de software y periféricos se mantuvo vivo gracias al entusiasmo de sus usuarios y de algunas pequeñas compañías que continuaron desarrollando software y produciendo hardware de manera semi-artesanal.

Pero los aficionados al QL seguían en contacto con las novedades del momento, y la revista QL World publicó en octubre y noviembre de 1986 una serie de 2 artículos sobre el protocolo MIDI, incluyendo el diseño de un interfaz MIDI para el QL que podía construirse el propio usuario.

https://dilwyn.theqlforum.com/mags/qluserworld/QLWorld_1986-10.pdf

https://dilwyn.theqlforum.com/mags/qluserworld/QLWorld_1986-11.pdf

En 1989, Miracle Systems produjo el QL MIDI Pack, compuesto por un interfaz MIDI para el QL que se conectaba a través del puerto ROM y un software secuenciador MIDI, el “QL Tracker”, que permitía tanto controlar un sintetizador MIDI como grabar en el QL secuencias MIDI desde un instrumento MIDI externo.

La revista QL World publicó una reseña en el número de febrero de 1989, donde también apareció anunciado. Su precio era de 78 Libras. https://dilwyn.theqlforum.com/mags/qluserworld/QL%20World%20February%201989.pdf

El boletín del Toronto Timex-Sinclair Users Club, incluyó una reseña de interfaz QL MIDI en su número de marzo de 1990 (Sinc-Link, Vol 8 Num 2 – pag. 15)

https://archive.org/details/SincLink/Sinc-Link%20Vol%2008%20No%202/page/15/mode/1up

Desafortunadamente, aunque se han preservado tanto el layout y el esquemático del hardware como el software “QL Tracker”, no se ha podido recrear el QL MIDI, y no se tiene constancia de ninguno en funcionamiento.  

El QL MIDI Pack fue el único producto comercial relacionado con el protocolo MIDI para el Sinclair QL.


No fue hasta 10 años más tarde que algunos usuarios del QL retomaron el interés por el protocolo MIDI en el QL, aprovechando el puerto QLnet del QL como interfaz MIDI, utilizando un cable como el del esquema. Financiado por el NESQLUG (New England Sinclair QL Users Group), Simon N. Goodwin desarrolló el Toolkit DIY_MIDI, que contiene varios comandos para controlar instrumentos MIDI desde el QL. Este toolkit fue presentado en el US East Coast QL Show el 29 de mayo de 1999.

Utilizando los comandos del DIY_MIDI Toolkit, Al Boehm desarrolló un software reproductor de archivos MIDI para QL con el poco original nombre de “MidiPlayer”. La primera versión fue publicada en diciembre de 1999, y la segunda en febrero de 2000. Este programa reproduce archivos MIDI estándar y tiene un interfaz de usuario basado en las ventanas de texto del QL. El mismo Al Boehm publicó un breve artículo en QL Today vol.4 issue 6 explicando la versión 2.

Este programa permite seleccionar y cargar un archivo MIDI de disco, lo analiza y genera la secuencia de comandos necesarios para reproducirlo enviando las correspondientes instrucciones al sintetizador MIDI por el puerto QLnet.

Finalmente, en agosto de 2002, el usuario de qlforum Silvester publicó el driver MNET para poder utilizar el puerto QLnet del QL como MIDI OUT. El programa “Quaver”, del mismo autor, se sirve de este driver para reproducir archivos midi en un sintetizador externo conectado por el puerto QLnet.

https://www.theqlforum.com/viewtopic.php?f=2&t=1587&hilit=qsound&start=10#p14069

Una vez repasada la historia del MIDI y el QL, es el momento de verlo en funcionamiento. Para ello necesitamos hacernos un cable según el esquema de más arriba, y por supuesto, un sintetizador MIDI.

Lo ideal sería tener un Roland MT-32 o equivalente, pero una búsqueda rápida en los sitios habituales me hizo desistir debido al precio de los mismos, por lo que hubo que buscar una alternativa. Después de explorar un poco, me decidí por un mt32-pi hat y una Raspberry Pi 3a+ , que en total costaron menos de 50€. El proyecto mt32-pi es un emulador baremetal de Roland MT-32 disponible en github ( https://github.com/dwhinham/mt32-pi ). También de github es el proyecto del hat que he utilizado ( https://github.com/chris-jh/mt32-pi-midi-hat ).

La Pi 3A+ no funcionó directamente con los archivos del repositorio del github, pero tras un poco de investigación, encontré la solución en este post de un foro de MISTer ( https://misterfpga.org/viewtopic.php?t=7427 ).

Con el cable y el mt32-pi a punto, pude probar los dos reproductores de MIDI.

Sin profundizar mucho en las pruebas, me quedo con el “Quaver”. Es más fácil de utilizar y se puede ejecutar en multi tarea, como un job del sistema operativo. Lo que abre la puerta a pensar en juegos para el QL con banda sonora MIDI….. Todo se andará.

QL Forever!

Wednesday, 29. October 2025

Thursday, 16. October 2025

Thursday, 02. October 2025

DOOM para Sinclair QL

21:47 GMT (12 months ago)

El domingo por la noche nos dieron la pista en Amigawave…… Un video de un port del Episodio 1 de Doom corriendo en un emulador de SInclair QL para Windows bastante antiguo (QL2K)

Según se puede leer en el GitHub del autor (https://github.com/FrenkelS/Doom8088ST ) del autor, es un port de Doom para CPUs 68000, desarrollado a partir del Doom8088. Está escrito en C, con algunas partes en ensamblador. Hasta el momento, ha hecho releases para Atari ST, Sinclair QL y AT&T UNIX PC.

Para el QL ha compilado dos versiones (8 colores y mococromo en B/N). Corre en un QL com 512k de RAM, pero va bastante lento. En un QL con Super Gold Card (68020 a 24Mhz y 4MB de RAM) va de lujo. Funciona en todos los emuladores de QL y SMSQ/E y también en el core de QL para el Spectrum Next.

Los ejecutables que se pueden descargar desde el GitHub (https://github.com/FrenkelS/Doom8088ST/releases) son para el emulador QL2K. Para QL real u otros emuladores, hemos creado un archivo .WIN, que debe montarse como win2_, y ejecutar el comando exec_w win2_doomql8q

Otros compañeros lo han probado en diferentes configuraciones, pero yo sólo tenía a mano el N-GO y lo he podido probar en el core de QL para Spectrum Next:

A pesar de las limitaciones a nivel visual, es muy jugable y, sobre todo, es casi increíble…

Este 2025 hemos visto en el QL el Batman de Jon Ritman y ahora el DOOM, ¿qué será lo próximo?

QL Forever

Sunday, 28. September 2025

Microdeal y el QL: historia de un fracaso

22:00 GMT (12 months ago)

John Symes fundó la empresa Microdeal en 1982, en St Austell, Cornwall, en Inglaterra. Al principio no eran mas que él, una secretaria y un comercial y se dedicaban a importar juegos de ordenador desde Estados Unidos, que adaptaban al Dragon 32, que había salido al mercado en 1982, para venderlos por correo.

En marzo de 1984 disponían de un catálogo considerable para el Dragon 32, como puede verse en este anuncio:

El Dragon fue la principal plataforma para la que editó software Microdeal, llegando a publicar más de 100 títulos para este ordenador, incluyendo juegos, utilidades y software educativo.

Hacia la segunda mitad de 1984, Microdeal era una de las principales editoras de Software británicas. Contaba con más de 40 empleados, una flota de vehículos para distribuir cintas y diskettes a lo largo y ancho del país, un almacen con sistema de gestión de stocks informatizado y más de 120 referencias en catálogo para distintos ordenadores (Dragon, Commodore 64, Commodore 16, Spectrum e incluso Enterprise).

En ese momento, el Sinclair QL, que había sido presentado en Enero, y había tenido un tortuoso lanzamiento comercial, con retrasos en las entregas, problemas con las ROM del sistema (el famoso kludge que había que conectar en el puerto ROM de los primeros QL) y problemas varios de calidad. Además, Sinclair pretendía comercializar el QL como un ordenador 100% profesional, muy alejado de los juegos de niños de los que rengaba profundamente. Este hecho provocó que las principales empresas de softtware lúdico de la época en el Reino Unido dejaran de lado el desarrollo de juegos para el QL. Empresas como Imagine (BC Bill, Cosmic Cruiser, Arcadia), Bug-byte (Manic Miner) o Addictive (Football Manager) no portaron ninguno de sus éxitos a la nueva máquina de Sinclair. Probablemente, el hecho de tener que comercializar el software en formato Microdrive, único formato de almacenamiento disponible para el QL, y más costoso de producir que la onmipresente cinta de cassette, tampoco ayudó a que estas compañías ni siquiera intentaran probar como podía funcionar el mercado de videojuegos para el QL.

Pero en 1985, Microdeal apostó por la nueva máquina de Sinclair, una vez esta se había consolidado en el mercado con unas modestas, pero no despreciables, 50 o 60 mil unidades vendidas. En el número de QL User correspondiente a Agosto de 1985, Microdeal publicó un anuncio a toda página con sus primeros lanzamientos para QL: dos ports de antiguos exitos para Dragon (Cuthbert in Space y QL Hooper) y una aventura gráfica de nuevo cuño (Lands of Havoc).

Ese mismo mes de Agosto, aparecieron en las revistas especializadas las primeras reseñas de juegos de Microdeal para QL, Lands of Havoc en QL User y Sinclair User.

Y ya en Septiembre, en su catálogo de Otoño/Invierno 1985, Microdeal desvelaba los resultados de sus primeros moviemientos y sus planes para el QL: las ventas de sus tres primeros lanzamientos habían sido muy satisfactorias, teniendo que aumentar la producción dado el éxito de los tres títulos en la ZX Microfair de Junio. Además, anunciaba el nuevo lanzamiento que estaba en preparación: Crazy Painter, otro port de un antiguo éxito para Dragon.

En su número de Noviembre, QL World dedicó casi en exclusiva a Microdeal su sección de software lúdico, con reseñas de los cuatro juegos publicados hasta el momento. En Diciembre, QL User publicó un extenso reportaje de 5 páginas sobre los juegos disponibles para QL incluyendo reseñas de los cuatro títulos de Microdeal. Las ventas parecían ir viento en popa y las publicaciones especializadas se hacían eco, con valoraciones positivas, de sus propuestas. Era el momento de lanzarse a conquistar el mercado enfrentando a la competencia que representaban Psion (Chess, Match Point), Shadow (Quasimodo, Star Guard), Talent (West, Zkul) o la propia Sinclair (Cavern, Meteor Storm).

Microdeal contrató dos anuncios a página completa en QL World y QL User para los números de Diciembre, de cara a la campaña de Navidad. En ambos se destacaba el que iba a ser el lanzamiento definitivo de la compañía para esa campaña; en QL User, junto al resto del catálogo en una página a todo color y, en QL World, un anuncio a página completa para dicho título: Flight Simulator.

Este mismo anuncio del Flight Simulator volvió a aparecer en QL User (Enero 1986), Personal Computing Weekly (16 de Enero 1986) y QL World (Marzo 1986). La única reseña (muy positiva) del juego apareció en PCW, en el número correspondiente al 9 de Enero de 1986.

Todo parecía ir bien para Microdeal con el QL, pero en realidad no es oro todo lo que reluce. Las ventas del ordenador no despegaban y, por si fuera poco, en Abril se anuncia la venta de Sinclair a su rival Amstrad y empiezan a surgir las dudas sobre la continuidad del sistema QL. Ese mismo mes de Abril, Microdeal anuncia una rebaja significativa en los precios de sus títulos para QL con un anuncio en QL World que, tras la fusión con QL User el mes anterior, quedaba como la única publicación comercial dedicada al QL. El declive del mercado empezaba a anticiparse y Microdeal intentaba liquidar su stock de juegos en microdrive.

A pesar de los malos augurios que aparecían en la prensa especializada sobre las dificultades que CST y Sandy estaban teniendo para comercializar sus ordenadores continuadores del sistema QL, Thor y Futura respectivamente, por el nulo interés de Amstrad en transferir la Propiedad Intelectual que les permitiera continuar evolucionando la plataforma, la vida seguía ahí fuera y los usuarios del QL se empeñaban en mantenerlo vivo. QL World continuaba editándose y vendiéndose bien y las ventas de software mantenían unos niveles aceptables, aunque no comparables a los sistemas de más éxito en UK (ZX Spectrum, Commodore 64 y Amstrad CPC). Microdeal tenía algunos proyectos para QL en fase de desarrollo y decidió seguir adelante con su comercialización. En la QL World de Julio apareció un anuncio a página completa con dos nuevos lanzamientos.

The King es una versión «no oficial» del archiconocido Donkey Kong, que Microdeal había publicado para Dragon en 1983 y Aquanaut es una aventura gráfico-conversacional que se publicó al mismo tiempo para otros sistemas como Dragon y BBC Micro. En los meses de Julio, Agosto y Octubre, QL World publicó dos reseñas distintas de Aquanaut y otra de Crazy Painter.

La vida comercial del QL estaba llegando a su fin por lo que respecta a las grandes empresas y el mercado de consumo. En Diciembre de 1986, QL World publicó un nuevo anuncio de precios rebajados de Microdeal, que se repitió en Enero y Febrero de 1987.

La tercera y última publicación de este anuncio concidió en la revista con una reseña semi-clandestina de la última producción de Microdeal para el Sinclair QL: Stone Raider II (aunque nunca hubo un primer Stone Raider), en la que, además, la foto de una pantalla que ilustraba el artículo estaba cruzada con la foto de un programa de recetas de cocina de otro artículo. El juego, que recibía una valoración positiva, no era más que un clon del clásico Boulder Dash. Eso sí, muy bien adaptado al QL.

Esa fue la última mención a un producto de Microdeal en la revista QL World. En los meses siguientes, algunos juegos de la casa aparecían en los listados de programas comercializados por los distribuidores como Strong Computer Systems (por ejemplo en Abril de 1987). Pero en la revista de Mayo, ninguno de esos juegos aprecía en el listado, señal inequívoca de que Microdeal había dejado de distribuirlos, ina vez liquidado el stock.

QL World – Mayo 1987

Microdeal dejó de producir y distribuir software para QL en Abril de 1987. En Diciembre de ese año, anunció que abandonaba el Dragon, que había sido su plataforma más prolífica, para centrase en la producción de software para las máquinas de 16 bits (Atari ST y Amiga). EL mercado de los 8 bits estaba decayendo.

Muchos de los títulos de 16 bits de Microdeal fueron versiones actualizadas de exitosos juegos de 8 bits como Time Bandit y Tanglewood, pero tuvieron menos éxito en esta nueva edición. A este le siguió The Karate Kid Parte II: The Computer Game, basado en la película de 1986. Los últimos juegos de Microdeal aparecieron en 1989 y la compañía cesó su actividad en los primeros años 90, tras publicar unos 40 títulos para Atari ST y unos 20 para Commodore Amiga.

Los títulos de Microdeal para Dragon, Atari, Amiga y otras plataformas están preservados y accesibles en diferentes webs y repositorios en internet como AtariMania y DragonArchive. Los derechos del software de Microdeal para QL pasaron por diferentes manos hasta llegar a Tom Dolezal (Talent+), quien mantiene los derechos y no ha autorizado su libre distribución, por lo que no están disponibles para descarga. De todas maneras, si estás interesado, deja un comentario.

Crazy Painter – Cuthbert in Space – QL Hooper – Lands of Havoc – Flight Simulator – The King

Microdeal fue una de las grandes empresas británicas de software para microordenadores de la primera época y, aunque intentó evolucionar y adaptarse a los nuevos tiempos, algunas decisiones equivocadas (como apoyar al Dragon y al QL) y la dificultad de adaptarse al mercado de 16 bits la llevaron a la desaparición. Como Imagine, Bug-byte, Quicksilva y otras grandes de los primeros años, no supo ni pudo sobrevivir, una historia paralela a la de Sinclair Research y su línea de microordenadores, que empezó muy bien con el ZX80 y el ZX81, alcanzó un éxito absoluto con el ZX Spectrum y no pudo ni supo sobrevivir a partir de una serie de decisiones empresariales cuando menos cuestionables (el lanzamiento del QL, el desarrollo del C5).

La evolución cronológica de Microdeal como editor de software para QL es una muy buena muestra de como fue la vida comercial del ordenador de Sinclair desde su lanzamiento hasta su cancelación por Amstrad. A partir de 1987, el QL mantuvo vivo gracias a sus usuarios, ya sea en UK (QUANTA, NESQLUG), en España (QLave,CUQ, QLiper), o en Alemania (Quasar), al equipo de QLToday, y gracias a una serie de aficionados que continuaron desarrollando y distribuyendo software fuera de los circuitos comerciales del mercado de masas. Y así ha seguido hasta la actualidad. EL QL se mantiene vivo gracias a sus usuarios, y que así siga por mucho tiempo….

QL Forever!

Friday, 12. September 2025

RenumQB

16:14 GMT (over a year ago)

When I was porting the Dracula adventure to QL, to help with the process of converting programs from older PC BASICs to QL, I wrote a little utility called “RenumQB” to help with automating the renumbering of older programs, many of which seemed to be written in BASICs which had no RENUMber command.

Those listings may have had weird line numbering, or used ‘labels’ instead of line numbers, or even no line numbering at all.

And when a listing is full of randomly numbered GOTOs and GOSUBs, the code can be a right royal spaghetti which is hard to follow.

So, provided you’ve got a plain text listing of the program, RenumQB will make a list of lines, GOTOs and GOSUBs and try to sensibly and tidily renumber them for porting to QL, including replacing labels with line numbers (and inserting references to the original labels to help refer to the original listing), and correctly re-labelling GOTOs and GOSUBs (although it can’t cope with computed GOTOs such as GOTO 5000*X).

Far from perfect, but I found it helped a lot when converting BASIC programs to QL.

Note: uses the QLiberator extensions Q_ERR_ON and Q_ERR_OFF. You may already have those extensions installed if you use the QLiberator compiler. If not, I’ve included a copy in the zip file (just LRESPR the Q_ERR_BIN file) along with an instructions DOC file and an example BASIC program to run through RenumQB.

Get it from https://dilwyn.theqlforum.com/basic/renumqb.zip

Wednesday, 10. September 2025

Dracula

15:44 GMT (over a year ago)

Dracula is a horror text adventure I’ve ported to the QL. Originally written by Elizabeth Arkush in 1984, it’s been ported to SuperBASIC and slightly enhanced.

You are in Dracula’s mansion, searching for the fabled and aptly named Blood Ruby. Beware of bats, werewolves and of course Dracula himself.

Only two things can slay Dracula – a silver bullet, or a wooden stake through the heart.

Things to keep in mind while playing:

  • there are many secret doors in the mansion you have to discover how to open
  • Dracula leaves a trail of bloody footprints wherever he walks
  • Dracula can turn into a bat and can bite necks in that condition
  • ordinary bats may be harmless (may be…)

I’ve added game SAVE and LOAD state commands so that you can pause a game and resume it later if you wish.

Please read the DRACULA_txt instructions file first.

Should you decide you are stuck and need a little help, I’ve added the following files:

MAP_TXT – a text map of the mansion.
HINTS_TXT – a few fairly cryptic hints here and there in the adventure.
SOLUTION_TXT – a step by step solution to resort to when completely stuck.

Good luck. You may need it when up against Dracula!

Download free from https://dilwyn.theqlforum.com/games/adventures/index.html

Sunday, 17. August 2025

Q-Liberator v3.46 Update

12:45 GMT (over a year ago)

Per Witte has supplied a minor update to the Q-Liberator BASIC compiler distribution.

This latest release of v3.46 contains no functional code change to the compiler itself from the previous release. The main difference is Martin Head’s comments in the toolkit sources. a few changes to the documentation in the zip files. No changes to the main manuals on the website.

https://dilwyn.theqlforum.com/qlib/index.html

To make sure I get the full description right, here’s Per’s notes on the individual zip files making up the distribution set:

Q-Liberator v3.46
The Q-Liberator compiler and compiler source code.
This distribution comes in the basic form of two disks:

  1. Qlib346u is a 720k floppy containing all you need to compile and run Q_Liberator V3.46. The u stands for User. It is distributed thus: Qlib346ui.zip is the zipped version of an image of this disk. Once unzipped, the image can be mounted directly into most emulators and the software run from there. Qlib346ud.zip is the zipped contents of the User disk. Unzip it to a suitable location yourself, such as a floppy disk or hard drive. The device’s driver must support directories! Qlib346u.zip is the zipped contents of the User disk without directories. Unzip it to a suitable floppy or RAM disk (it wont fit on a microdrive!)
  2. Qlib346s is a 1440k disk containing the Q_Liberator source files. It is not required to compile or run Qlib programs. It is to better understand Qlib and, hopefully, contribute towards future updates and improvements.
    The s stands for Source. It is distributed thus: Qlib346si.zip contains the 1440k floppy image of the source disk. Once unzipped, the image can be mounted directly into all the best emulators and used from there. Qlib346sd.zip contains the contents of the Qlib346s source disk and can be unzipped to any suitable location, like a subdirectory on your hard disk.

Note: The zipped image files (*ui.zip and *si.zip) should be unzipped using an external unzip utility, ie from Linux, Windows, etc. The remaining zip files are QL files and should be unzipped in a QL environment with a suitable QL unzip utility.

Additional files:

The latest Q_Liberator manual is called Q_Liberator 3.36+ manual.zip, and must be downloaded separately, presumably from the same site as the User and Systems disks. (Not included here.)

There is also some accomanying technical information in ODT and PDF format in the zip file QlibTechNotes.zip. This was produced by Martin Head who analysed and commented on the various toolkits.

Picture of Q-Liberator front panel screen.

Wednesday, 06. August 2025

Minerva and Games

21:44 GMT (over a year ago)

The Minerva ROM brings a number of improvements to QDOS, but some QL games (especially early ones) don’t work correctly on it. What are the technical reasons for these problems?

For last year’s release of the “QL Games Collection” I worked on a special version of
Q-emuLator to use as the runtime for the games and I had the opportunity to investigate and answer this question for many games.

Here is what I found, the main differences between Minerva and Sinclair ROMs that can cause incompatibilities:

  • Minerva expects programs to run in user mode. If a program changes the supervisor stack pointer even just for a short while, Minerva can crash, for example when the interrupt 2 service routine is called.
  • Some games take complete control of the QL, typically to maximize speed and available memory, to use the second hardware display page or to strengthen the copy protection. However, virtually all of these programs still need to call QDOS to communicate with the IPC co-processor to play sounds and to read the keyboard.
    In Sinclair ROMs, the IPC functions are at a lower level than the rest of the OS, but on Minerva IPC access is more similar to any other QDOS calls and it accesses the system variables, which these games normally overwrite. This can cause crashes or funky behavior.
  • The interrupt 2 handler, which some games use to get the correct timing or to run code periodically, accesses some system variables both on Sinclair and Minerva ROMs. However, the Sinclair handler can often continue to work when the variables are overwritten, while the Minerva one can not.
  • Minerva treats unhandled exceptions very differently than the Sinclair ROM.
    It’s actually pretty common for QL software to contain bugs that cause unhandled exceptions as when running on Sinclair ROMs most of these exceptions were simply ignored by QDOS and many software authors didn’t even have a chance of noticing the problem.
    On Minerva, unhandled exceptions cause a call to the OS rather than just being ignored and this in turn can have unexpected side effects, or crash the system when combined with any of the previous issues (for example for games that take over the entire system).

In general, when running early QL games on an emulator, the safer choice is to use a Sinclair ROM and set the amount of RAM to 128 KB to avoid incompatibilities. A number of these games wouldn’t work if the QL had a RAM expansion as they expected everything to be at the same fixed memory addresses as on a 128 KB QL.

Saturday, 10. May 2025

Sinclair QL • Nuevo juego para Sinclair QL: Batman

13:41 GMT (over a year ago)
Bombazo que ha anunciado El Mundo del Spectrum hoy:
https://www.elmundodelspectrum.com/bomb ... nclair-ql/

Joan Gayón es el autor de la conversión, y aprovechando a tope el modo gráfico del QL (más colorines).
Funciona en un QL stock con ampliación de RAM a 640k, aunque también funciona perfectamente en QL con aceleradora, emuladores y FPGA.

Se puede descargar aquí:
https://sinclairqles.wordpress.com/2025 ... -download/

Imagen

BatmanQL – Joan Gayón (2025)

06:35 GMT (over a year ago)

Batman, el “Hombre Murciélago”, apareció por primera vez en la historia titulada «El caso del sindicato químico» de la revista Detective Comics N.º 27, lanzada por la editorial National Publications el 30 de marzo de 1939.

A partir de ese momento, Batman, el superhéroe, el trasunto del millonario Bruce Wayne que combate a los malhechores en Gotham City, se convirtió en un mito de la cultura popular a nivel mundial: cómics, series de televisión, relatos escritos, juguetes, películas y, a partir de 1986, videojuegos. Sí, Batman llegó al décimo arte en la época dorada de los 8-bits, y desde el primer momento supuso un hito, tanto a nivel comercial, como por sus logros técnicos.

Batman (1986), programado por Jon Ritman con diseño gráfico de Bernie Drummond y publicado por Ocean Software, llevaba más allá de los límites conocidos las posibilidades de los juegos en 3 dimensiones con perspectiva isométrica (la famosa técnica Filmation) que había popularizado el sello “Ultimate play the game” con los también míticos títulos Knight Lore (1984) y Alien 8 (1985).

Knight Lore (1984) – Alien 8 (1985) – Batman (1986)

El juego fue desarrollado originalmente para ZX Spectrum y, en la época, se lanzaron versiones para Amstrad CPC, MSX y Amstrad PCW. Posteriormente, aficionados de diversas plataformas han desarrollado remakes (MS-DOS) o versiones mejoradas (MSX2, con los gráficos coloreados).

El juego de Ritman y Drummond fue todo un éxito en su momento, cosechando altas puntuaciones y menciones de honor en las revistas inglesas (C+VG Hit, Crash Smash, Sinclair User Classic, Your Sinclair Megagame, Zx Computing Monster Hit). En aquella época, Microhobby no daba puntuaciones a los juegos reseñados en la revista, pero en el artículo de la página 12 del número 83 incluyó perlas como éstas: “se trata de una extraña mezcla entre los aspectos gráficos de Fairlight y la técnica de juego en la más pura línea arcade de los míticos programas de Ultimate” o “Batman no necesita equipararse a nadie; su calidad habla por sí misma, tiene madera de estrella”.

A nivel comercial fue un éxito total. No se dispone de cifras de venta concretas, ni en España ni en UK, pero en la lista “los 20+”, que publicaba semanalmente microhobby con datos proporcionados por El Corte Inglés, se mantuvo más de 20 semanas, que es muchísimo tiempo teniendo en cuenta que fue publicado antes de la bajada de precios de Erbe. El PVP de salida de Batman fue de 2.100 pts.

A nivel técnico era un alarde absoluto, llevando la técnica Filmation de perspectiva 3D isométrica a su máxima expresión. Las 150 pantallas plantean al jugador una serie de puzzles complejos pero resolubles, con un movimiento de personaje y enemigos suave y fluido, sin ralentizaciones de ningún tipo. El diseño gráfico de Drummond es detallado y preciosista, monocromo en el Spectrum, el MSX y el PCW y algo más colorido en la versión de Amstrad CPC. El diseño de la jugabilidad fue otro gran acierto y, a diferencia de la gran mayoría de juegos de la época, sigue siendo muy jugable aún hoy.

Batman apareció en mayo de 1986. El mercado de la informática doméstica ya había dejado atrás el primer boom. Las plataformas de 8-bits más exitosas se habían consolidado (Spectrum, Amstrad, Commodore y MSX) y muchas otras se habían quedado en el camino (Oric, Dragon, Sord, New Brain, etc.). Los ordenadores de 16-bits ya habían aparecido en el mercado (Atari ST, Amiga) aunque todavía no representaban una parte significativa del mismo.

Otra víctima ilustre de los vaivenes del mercado (o de la falta de acierto de su creador) fue el sucesor del Sinclair ZX Spectrum, el ZX83, o como fue finalmente lanzado en enero de 1984, el Sinclair QL. Sinclair lanzó al mercado su Quantum Leap como ordenador profesional, pero con prestaciones de ordenador doméstico, con lo que al final no fue ni una cosa ni otra. Los problemas de calidad y retrasos en el lanzamiento unidos a la fuerte competencia provocaron que, tras el colapso de Sinclair y la consiguiente venta de su IP de ordenadores a Amstrad en abril de 1986, Alan Sugar decidiera cancelar la línea QL y mantener únicamente el Spectrum en su portfolio de productos.

O sea, que el QL murió en abril de 1986 y Batman salió al mercado en mayo del mismo año. Como es fácilmente imaginable, nunca hubo una versión de Batman para el malogrado ordenador de Sinclair.

Pero nunca es tarde si la dicha es buena, y en pleno siglo XXI, la denostada plataforma de Sinclair cuenta con una legión de adeptos, bueno, más que legión quizá no sea más que una centuria, que mantienen vivo el espíritu de la máquina con nuevos desarrollos de Hardware y Software y proporcionan inesperadas alegrías a los aficionados.

Hace más de 4 años, un programador de pura cepa descubrió las posibilidades gráficas (limitadas, 8 colores en 256×256 píxeles) del QL y se preguntó cómo luciría uno de los juegos favoritos de su infancia (el Batman de Jon Ritman para Spectrum) a todo color. Dicho y hecho, Joan Gayón tomó el toro por los cuernos: se compró un Sinclair QL, estudió a conciencia el funcionamiento de la memoria de video, las interrupciones, los registros, el (casi inexistente) sonido y se puso a programar en ensamblador del Motorola 68000 un clon  pixel perfect del juego original. Bueno, no exactamente pixel perfect, ya que también convirtió los gráficos monocromáticos del original de Spectrum a una versión de los mismos en glorioso multicolor, aprovechando al límite la escasa paleta del QL.

Y por fin, tras doce párrafos de verborrea insulsa, ha llegado el momento de decirlo alto y claro, ni Match Point, ni Alien Hijack, ni nada que se le acerque. Joan Gayón ha programado EL MEJOR JUEGO DE TODOS LOS TIEMPOS PARA EL SINCLAIR QL, y además tenemos la suerte de que lo pone a nuestra disposición para que lo disfrutemos. Funciona en un QL con 640k de RAM (los 128k originales de la máquina más 512k). El equipo de beta-testers ha verificado el funcionamiento en distintas configuraciones, tanto de máquina física (ROM JS o Minerva, Gold Card, Super Gold Card, Aurora) como en distintos emuladores (sQLux, QemuLator, ZesarUX) y soluciones FPGA (Mister).

El juego se comporta exactamente igual que el original, los mismos gráficos (en colorines), el mismo mapa, los mismos puzzles, la misma diversión.

Absolutamente EspectaQLar!!!!

Friday, 09. May 2025

Thursday, 13. February 2025

QL Forum Move

11:37 GMT (over a year ago)

I know I said I wasn’t going to post much more here, but this is quite important.

The QL Forum is moving! There is a post on the forum that explains the reasons why it is moving. The change should happen 22nd-23rd February if all goes well.
https://qlforum.co.uk/viewtopic.php?t=5143
The new address will be https://theqlforum.com/

As my own site is hosted there, as well as one or two other QL-related sites, my site will have a new address of dilwyn.theqlforum.com , or you can click on the “QL Homepage” link at the top of the QL Forum page, highlighted in the picture below. If the display is too narrow on your system, it’ll be in the Quick Links menu on the left.

QL Homepage link on QL Forum

Tuesday, 07. January 2025

Blog Paused

09:28 GMT (over a year ago)

This blog is now closed, paused for at least for the time being while I dwell on the future.

I can no longer cope with the attitudes and negativity of certain people in the QL community. Just like much of the rest of the world, the QL community is becoming less tolerant of anyone’s views but their own, and less appreciative of the hard work of those who’ve contributed to 41 years of the wonderful little 1980s computer called the QL.

Time to retreat into the background, recharge the batteries and just enjoy the QL for myself for a bit I think.

Thank you to the majority nice guys. No thanks at all to the rest.

Wednesday, 25. December 2024

Rudolph Is Ill

18:08 GMT (over a year ago)

“Rudolph Is Ill” is a Christmas themed text adventure game, where Rudolph The Red Nosed Reindeer is too ill to deliver Christmas presents to the children of the world. You have to find the magic golden carrot to make him better in time to avoid disappointing the world’s children. The game is set at Santa’s place, where you have to deal with polar bears, arctic wolves and other problems that arise.

Written in SuperBASIC, should run on any QDOS or SMSQ system.

Download free from https://dilwyn.qlforum.co.uk/games/adventures/index.html

Friday, 13. December 2024

Saturday, 23. November 2024

ZB2SB. Conversor de Basic de los ZX al Super Basic del QL (I): Introducción

09:29 GMT (over a year ago)

Índice de entradas del conversor

Cambios en rojo el 23/11/2024


Como sabemos, los programas se escriben en un lenguaje de alto nivel, y usando otro programa se convierten en programas escritos en otro lenguaje. Cuando ese lenguaje es directamente código máquina (a veces es ensamblador que se ensambla y linka de forma transparente) tenemos los que habitualmente se llama un COMPILADOR.

Por definición, un COMPILADOR es un programa que convierte un programa escrito en un lenguaje, en un programa escrito en otro lenguaje. Cuando ese otro lenguaje no es código máquina (o ensamblador, que se considera lo mismo en este tema), sino que lo convierten en otro lenguaje diferente, se le denomina TRANSPILADOR.

Los viejos quizá recuerden el compilador Turbo Pascal y luego el compilador Turbo C (posteriormente Borland C), que eran ampliamente usados por generar no solo de forma mas rápida que otros compiladores del momento (recuerdo en un 386 esperar 20 minutos para que terminara de compilar un programa en ABAL), sino porque generaba un código bastante rápido al estar bien optimizado. Luego se pasó al Turbo C++, pero las primeras versiones usaban dos pasos, primero un pre-procesador convertía el código C++ en C estándar, y luego el compilador compilaba el programa en C. Esto hacía que depurar fuera un infierno, depurabas el programa en C que no era el que habías escrito tu en C++, lo que era un desastre. Por fortuna, fueron rápidos en sacar un compilador completo, que generaba ya directamente desde C++ código máquina y podías debugear sin problemas.

Partes de un compilador

Un compilador se divide en varias partes, cada una centrada en un aspecto de la conversión del fuente en el objeto:

  • El analizador Léxico toma el programa y lo descompone en sus unidades básicas, sentencias, variables, operadores, separadores, etc. Para hacerlo necesita unas tablas auxiliares con los elementos que debe reconocer, separadores, sentencias, formato de variables, formato de comentarios, formato de cadenas, etc. Cada uno de estos componentes se denomina un TOKEN, y tiene dos atributos, el token y su tipo.
    Por ejemplo si tenemos la instrucción 100 IF a>=b THEN GOTO 500 lo descomponemos en estos tokens: 
    • 100, número de línea
    • IF, sentencia de comparación
    • a, variable
    • >=, operador de comparación
    • b, variable
    • THEN, sentencia de ejecución
    • GOTO, sentencia de salto
    • 500, operador del salto
    • vacío, fin de instrucción (en lenguajes como C sería ";" en lugar de vacío)
  • El analizador sintáctico toma los Tokens que va recibiendo del léxico y monta lo que se denomina el árbol de sintaxis, que representa la estructura lógica de las sentencias, y unas tablas auxiliares con las variables, funciones, procedimientos y en los lenguajes que los usan los saltos que se han ido encontrando.
  • El Analizador semántico es el resultado de validar que el árbol de sintaxis es correcto.
  • El generador de código crea a partir de lo anterior el código de destino. En general se divide en tres partes, aunque puede usar una, dos o las tres partes dependiendo del lenguaje de destino principalmente si es o no lenguaje máquina, o de la complejidad del lenguaje destino, no es lo mismo un I7 que un pequeño microcontrolador PIC:
    • El generador de código intermedio crea a partir de la estructura lógica el código en el lenguaje de destino, o a veces en un lenguaje similar simplificado, en general genera lo que se denomina "código de tres direcciones", no mira mas que las sentencias que está recibiendo.
    • El optimizador mejora el código intermedio generando uno lo más rápido posible, usará las tablas auxiliares y puede tener en cuenta código precedente y posterior para eso.
    • El generador final convierte el código intermedio optimizado en el definitivo en lenguaje de destino.
El compilador actúa simultáneamente en las tres fases, no las hace una tras otra (salvo en algunos compiladores antiguos o cuando se desea ver lo que se genera de forma didáctica), en general el Sintáctico es el director, ya pidiendo tokens al léxico y va montando las estructuras, cuando tiene una sentencia completa llama al generador de código.

El diseño de compiladores actual utiliza programas que se han desarrollado, probado y optimizado para ayudarnos a generar el código necesario, estos programas los encontrareis escritos en C y en Java principalmente, como son Flex y JFlex para el léxico y para el sintáctico yacc, GNU bison y javaCC. El generador de código es lo que se programa realmente.

Notación BNF

No quiero ser muy académico (aunque para eso estamos los ingenieros informáticos), prefiero ser lo mas didáctico posible, pero solo por completar la definición, para poder compilar un lenguaje es primero necesario definirlo correctamente, para eso se utiliza la notación BNF.

Para definir el lenguaje a compilar, el señor John Backus, uno de los grandes pioneros, de los que antes de que existieran lenguajes de programación ya trabajaba con ordenadores en código máquina, para desarrollar el FORTRAN ideó una notación para describir la sintaxis del lenguaje, de forma que no hubiera problemas ni existieran ambigüedades. Este sistema fue ampliada por Peter Naur fijando lo que se conoce como Notación BNF. Para poder hacer algo con lenguajes de programación, lo primero que se debe hacer es definir el lenguaje de la forma más precisa y exhaustiva posible, para eso lo mejor es la BNF, o su equivalente en formato gráfico.


Objetivo

Mi idea en esta serie de entradas es intentar desarrollar algo mas sencillo de que un transpilador, ya que los lenguajes de origen y destino son muy similares y bastante compatibles, en su lugar mi idea es desarrollar en Super Basic un conversor que transforme programas escritos en ZX BASIC o Sinclair BASIC (se conoce por ambos nombres), que así se denomina el BASIC que fue inicialmente para el ZX80/ZX81, luego ampliado con nuevas sentencias en el Spectrum (el mismo en todos sus modelos, salvo comandos específicos para el +2 para cambiar el modo 48/ampliado o los de manejo del disco en los +3), en programas en Super Basic del QL. Digo intento porque espero acabarlo, ahora mismo lo tengo iniciado y genera código sin problemas para programas sencillos, pero le faltan todavía algunas cosas para considerarlo operativo.


Del compilador solo voy a usar el analizador léxico, que me retornará las sentencias separadas en Tokens, que convertirá a la sintaxis del SuperBasic.

En esta serie de entradas mi idea es reescribirlo mejorado, siempre que se reescribe un programa no partes de cero, sino de lo aprendido anteriormente, por lo que siempre se genera mejor código, y el que tengo no me convence actualmente, por lo que tengo excusa para empezar esta serie. Pero hay que empezar desde el principio.


Características del Basic de Sinclair

Me basaré es estas características que tienen todos los programas en Sinclair Basic, que en principio son comunes al SuperBasic:

  1. El programa lo leeremos desde un fichero de texto.
  2. Un programa se compone de líneas individuales, separadas por un terminador de línea * ver nota al final. Este terminador depende del origen del fichero que las contiene (si el origen es Windows será CR+LF, si el origen es Linux o el propio QL será LF, si estamos en Apple será CR). Por tanto lo primero que haremos es un proceso que lea un fichero de texto con el programa y lo descomponga en líneas, pasando el proceso para cada una de forma independiente.
  3. Cada línea del programa se compone de un número de línea, un espacio en blanco, y una o varias sentencias separadas por el carácter ":"
  4. Cada sentencia comienza por un identificador de sentencia, seguido por un espacio y una serie de modificadores. Por ejemplo la sentencia LET a=r-3 se compone del identificador "LET", seguido por el modificadores"a=r-3". 
  5. Existen sentencias simples como REM, LET o GOTO, y sentencias compuestas como pueden ser las decisiones IF/THEN o los bucles FOR/TO.
  6. Un comentario se define por el identificador de sentencia REM y su modificador abarca siempre hasta el final de la línea en que se encuentra. No hay comentarios multi-línea y por tanto tampoco anidados.
  7. Se aceptan nombres de variables con cualquier longitud.

Hay algunas sentencias que no se pueden usar en SuperBasic, por ejemplo PLOT, hay que analizar todas las sentencias y buscar su equivalencia, pero en general las principales diferencias entre ambos BASIC las podemos resumir en:

  1. El ZXBasic del Spectrum amplia el de los ZX80/81, por lo que incluye algunos comandos adicionales, como PAPER o INK. Por tanto todo lo aplicable a los programas para el Spectrum se debe poder aplicar a los programas para los ZX80/81.
  2. El ZXBasic utiliza variables numéricas sin atributo adicional, o de cadena con el atributo $. Super Basic se comporta igual, pero añade la posibilidad de usar variables enteras con el atributo %
  3. ZXBasic utiliza obligatoriamente la sentencia LET en las asignaciones, en SuperBasic es opcional.
  4. ZXBasic usa NEXT como final del bucle FOR, mientras que SuperBasic utilizar END FOR en su lugar, reservando NEXT dentro del bucle para otro comportamiento.
  5. No existe un cierre de los condicionales en el ZXBasic, por tanto estas instrucciones solo ocupan una línea. En este caso la sentencia "IF operador1 THEN sentencias", en la parte de las sentencias puede abarcar varias. Se admite lo mismo en SuperBasic, pero existe un fin de comparación por lo que es posible separar en varias líneas un IF.
  6. Las sentencias GO TO y GO SUB se escriben como una o como dos palabras separadas por un espacio, aunque sean una sola sentencia. En ZXBasic irán juntas y también en SuperBasic van en dos palabras separadas. En ZXBasic por un solo espacio, en SuperBASIC pueden usarse uno o varios espacios indiferentemente.
  7. La sentencia "PRINT expresion" admite varios operadores separados por coma o por punto y coma, algunos operadores NO los admite super Basic. Dentro de una sentencia PRINT la expresión puede ser uno de estos términos (algunos no se pueden usar en ZX80/81):
    • Quedar vacío
    • Una expresión numérica
    • Una expresión de cadena
    • AT m,n  >> Desplaza el curso a la línea-columnas. Esto no lo admite SuperBasic,
    • TAB n >> Desplaza el cursor al tabulador n. No admitido por SuperBasic
    • PAPER, INK, FLASH, BRIGHT,  INVERSE, OVER >> Atributos de color, que no admite SuperBasic
     
    Por esto, si tenemos por ejemplo:
    PRINT AT 4,12;INK 4;PAPER 5;"Hola ";INK 2;"que tal"
    Lo debemos convertir en:
    AT 4,12 : INK 4 : PAPER 5 : PRINT "Hola" : INK 2 : PRINT "que tal"

Limitaciones

El principal problema proviene de las diferencias entre los sistemas, no por los procesadores que sean diferentes, sino por las características propias de cada máquina, esencialmente en la pantalla y las llamadas a la ROM o a las variables del sistema desde el Basic.

Los ZX usan una pantalla con un tamaño y unos colores, mientras que el QL tiene varias resoluciones con varios modos de color. Aunque no es directo pero esto se puede ajustar creando una ventana en el QL.

Los ZX usan un juego de caracteres de 8x8, mientras que el QL usa varios juegos de resoluciones variables, y los comandos para redefinir el juego de caracteres del Spectrum no funcionan en el QL, que usa otras resoluciones y puede disponer de varios juegos, esto es complejo pero se puede llegar a solucionar.

Casi todos (por no decir todos) los comandos que se refieren al hardware del aparato no se pueden convertir, si en el ZX se hace un POKE para obtener una variable del sistema, lo más seguro es que esa variable no exista en el QL. Hay veces que se llama a una rutina en la ROM para acelerar el programa, esto no es posible replicarlo en el QL (no es el objetivo de estas entradas, pero siempre existe la posibilidad de escribir rutinas en código máquina que hagan algo equivalente), por tanto no se van a convertir comandos dependientes del Hard como PEEK o POKE.

(*) Nota sobre CR y LF. En las máquinas de escribir, el papel se insertaba en un rodillo que estaba ubicado sobre lo que se denominada Carro. Las teclas eran fijas, pero el carro se movía a izquierda o derecha, y el rodillo se movía hacia arriba o hacia abajo, de esta forma al pulsar una tecla esta presionaba una cinta entintada sobre el papel marcando un carácter, y luego movía el carro una posición a la izquierda para escribir el siguiente carácter. Si pulsábamos el espacio el carro se movía una posición a la izquierda sin imprimir nada. Al llegar al final de la hoja, había que hacer dos cosas, mover el carro completamente hacia la derecha y mover el rodillo una línea hacia arriba, esto se hacía con una palanca ubicada a la derecha, al apretarla primero movía el rodillo hacia arriba una línea (esto se podía usar siempre de manera independiente), si seguías apretando soltaba un freno y movías todo el carro hacia la derecha. Cuando se crearon los teletipos, esto se hacía de forma eléctrica, pero necesitaban dos comandos para hacerlo:

  • CR: significa Carriage Return (Retorno de carro). Al recibir este comando el sistema movía el carro completamente a la derecha.
  • LF: significa Line Feed (Avance de línea). Al recibir este comando el sistema movía el rodillo una línea hacia arriba.

Realmente se solía enviar primero el LF y luego el CR, aunque el orden es indiferente. En los sistemas operativos se adoptó una codificación u otra, según lo decidieron sus creadores, así:

  • En Unix (y todos sus derivados como Linux o BSD) se decidió que los ficheros de texto usaran solo LF para marcar el fin de línea. A la hora de imprimir se hacía con un programa que la controlaba y cambiaba en los teletipos LF por CR+LF. 
  • En CP/M se decidió usar CR+LF, que es lo que se remitía realmente a los teletipos al usar un controlador de impresión muy básico, del CP/M pasó al MS.DOS (a través del QDOS que era un clon del CP/M), y de este al Windows actual. 
  • Apple decidió lo de siempre, llevar la contraria a todos y usar CR únicamente. 
  • Los Mainframes de IBM usaban otro carácter diferente, denominado NEL, de NExt Line (línea siguiente). Otros sistemas como VMS no requerían usar fin de línea ya que guardaban los textos como registros.

Saturday, 16. November 2024

ZB2SB. Conversor de Basic de los ZX al Super Basic del QL (III): Analizadór léxico segunda parte: Extractor de Tokens

17:09 GMT (over a year ago)

Índice de entradas del conversor


Expresiones Regulares, Gramáticas regulares, Autómatas finitos, Autómatas de Pila, Máquinas de Turing, estos son algunas de las cosas que se emplean en la descomposición en tokens de los programas. Como he dicho que no voy a ser muy académico (y también porque no existen el manejo de expresiones regulares en SuperBASIC), voy a desarrollar la descomposición de Tokens usando un pequeño autómata.

No os voy a aburriros explicando lo que son los autómatas y como se programan, aunque es un tema importante en Informática, por ejemplo si leéis el manual del Kenbak-1 veréis que John Blankenbaker lo planteó como un autómata. De todas formas hay información de sobra en Internet sobre estos temas, y centrándonos en lo que estoy desarrollando podéis consultar libros sobre Compiladores, que hay muchos, de los cuales os recomiendo los dos que más se usan en las universidades sobre la materia: "Compiladores, principios, técnicas y herramientas" de Aho, Sethi y Ullman, también llamado "El libro del dragón" por su portada, de los varios que he leído creo que es el más claro y que más me gustó, el otro es "Construcción de compiladores. Principios y práctica" de Louden, aunque personalmente me gusta menos por no explicar tan bien como el otro, pero la parte de generación de código creo que es un poco mejor.

En lugar de eso voy a hacer un autómata muy sencillo que será fácilmente comprensible sin necesidad de muchas explicaciones. Así gano dos cosas, que no os canséis y no sobrecargar al QL de procesos, ya que aunque dispone (para mi) del mejor Basic con diferencia de las máquinas de la época, mi muy querido QL nunca se ha lucido por su velocidad. Por ser honestos, es superado por el BBC Basic, pero este tuvo una evolución importante con varias versiones, lo que no ocurrió con SuperBasic. Ambos fueron mucho mejor que los basados en Microsoft BASIC, de los que la mejor versión fue el MSX Basic, y desde luego mucho mejores que el GWBasic del MS.DOS en los PC. 

El BBC fue desarrollado por uno/una de los grandes de la informática, Roger Wilson, que luego cambió de sexo y hoy es Sophie Wilson (lo que personalmente me parece estupendo el que cada uno haga lo que desee, menciono ambos nombres por si los buscáis en Internet), que no solo desarrolló el BBC Micro y su Basic, sino que desarrolló para IBM los primeros procesadores RISC, luego usados en estaciones de trabajo de Acorn, unas máquinas estupendas pero poco conocidas.

Volviendo al tema, este es mi autómata:

tipo = Get_Tipo(c$)
SELect ON tipo
  = TIPONUM : Extrae_Numero(c$)
  = TIPOALFA : Extrae_Identificador(c$)
  = TIPOCADENA : Extrae_Cadena(c$)
  = TIPOSEPARADOR: Extrae_Separador(c$)
  = TIPOOPERADOR : Extrae_Operador(c$)
  = TIPOOTRO : Extrae_Desconocido(c$)
END SELect

 

Como vemos toma un carácter de la línea que estamos analizando, mira de que tipo es, y según este tipo va a llamar al proceso que extrae el token de ese tipo de la cadena, con la salvedad de que cuando extrae un identificador este puede ser: 

  • Un comando del BASIC. En este caso si además el comando es el comentario extrae también el resto de la cadena como texto del comentario.
  • En otro caso será el nombre de una variable.

He desarrollado un módulo para probar la extracción de Tokens del analizador léxico de forma independiente al resto del desarrollo, podéis descargarlo desde aquí

En la siguiente entrada ya entramos en la conversión, ampliaré este módulo para incluir las pruebas de conversión, ya más adelante uniré el módulo de lectura de líneas con este para comenzar a analizar programas.

Para poder convertir haré una mejora de la parte donde identifico los comandos del ZX Basic, que ahora mismo es una cadena de caracteres con estos separador por un espacio, la tengo que convertir en una tabla que identifique el comando y el proceso que se usará para convertirlo.

Thursday, 14. November 2024

ZB2SB. Conversor de Basic de los ZX al Super Basic del QL (II): Analizadór léxico primera parte, lectura de líneas

12:33 GMT (over a year ago)

Índice de entradas del conversor


Comenzamos con el analizador léxico, esta lo divido en dos partes para simplificar, por un lado la lectura de líneas y por otro su descomposición en Tokens.

Partimos de que el programa de origen será un fichero de texto guardado en el Microdriver (o equivalentes como las unidades con una SD, un disquete, un disco duro, o desde un emulador un directorio en el equipo). El resultado serán líneas completas que pasaremos a la rutina que la descompone en Tokens. En esta entrada presentaré las rutinas de lectura del fichero fuente y las de escritura del fichero de destino, que son bastante sencillas realmente.

Para las pruebas el programa simplemente copiar la línea leída en el fichero de origen y la guardará tal cual en el fichero de destino. En otras entradas iré desarrollando esta parte.

En esta primera versión del programa incluyo la base del sistema, compuesta de una serie de procedimientos y funciones que iré llamando, usando este procedimiento:

1170 DEFine PROCedure Main
1180   Init        : REMark Inicializar variables globales
1190   Pantalla    : REMark Pantalla de petición de datos
1200   :
1210   REMark Preparar una ventana para ver los resultados
1220   :
1230   OPEN#cCon,con
1240   WINDOW #cCon,400,140,50,70 : BORDER #cCon; 2,2
1250   :
1260   Proceso     : REMark procesar el fichero de entrada
1270   :
1280   INK #cCon;6 : PRINT #cCon;LF$;"Finalizado"
1290   CLOSE #cCon
1300 END DEFine
 

  • Init: Este procedimiento inicializa las variables globales que usaremos. Al no existir contantes en SuperBASIC, usaremos variables para simularlas, por haber programado en C tengo la costumbre de que las contantes se escriban con nombres en Mayúsculas siempre.
  • Pantalla: Este procedimiento presenta la pantalla inicial, y solicita los nombres de los ficheros de origen y destino.
  • El OPEN y WINDOW crea una ventana para ir presentando las líneas leidas y las generadas
  • Proceso: Este es el procedimeinto que realizará todo el proceso de conversión, en este momento lo único que hace es copìar la entrada sobre la salida para las pruebas de lectura y escritura de ficheros.
  • Luego se presenta el mensaje de fin.

Veamos los procesos principales, comenzando por el denominado Proceso que es el que hace todo el trabajo de dirección:

1850 DEFine PROCedure Proceso
1860   LOCal fin,salida$
1870   :
1880   OPEN_IN  #cIn, fIn$
1890   OPEN_NEW #cOut, fOut$
1900   CLS #0 : REMark Por si hay un mensaje de sobreescribir
1910   :
1920   REMark --- Lectura del fichero de entrada
1930   :
1940   REPeat BucleLectura
1950     :
1960     fin = LeerFichero
1970     linea$=Trim$(linea$)
1980     IF (linea$ <> "") THEN
1990       nrl = nrl + 1
2000       Mostrar_Entrada linea$
2010       salida$=Montar$(linea$)
2020       Mostrar_Salida salida$
2030       Guardar(salida$)
2040     END IF
2050     IF fin THEN EXIT BucleLectura
2060     :
2070   END REPeat BucleLectura
2080   :
2090   REMark Finalizar el proceso
2100   :
2110   CLOSE #cIn
2120   CLOSE #cOut
2130 END DEFine Proceso

Este proceso comienza abriendo los canales para lectura y escritura asociandolos a los ficheros que usaremos, al llegar a la línea 1890, si el sistema encuentra que el fichero de salida ya existe nos pregunta si deseamos sobre-escribirlo. si usamos Minerva o algún Toolkit existe la posibilidad de evitar esto indicando que deseamos sobre-escribir siempre, pero yo prefiero usar la ROM estándar únicamente en estas entradas.

Luego llamará al procedimiento que lee el fichero línea a línea, presentando la línea en la pantalla, llama al proceso que Montará la línea de salida (repito en este momento solo copia origen a destino), y nos presenta la línea generada.

Al final cierra los canales de entrada y salida.

2530 DEFine FuNction LeerFichero
2540     LOCal car$
2550     :
2560     linea$=""
2570     REPeat Bucle_Lectura
2580         IF (EOF(#cIn)) THEN RETurn TRUE
2590         :
2600         car$=INKEY$(#cIn)
2610         :
2620         IF (car$ <> CR$) AND (car$ <> LF$) THEN
2630           linea$ = linea$ & car$
2640         ELSE
2650           IF (linea$<>"") THEN
2660             EXIT Bucle_Lectura
2670           END IF
2680         END IF
2690     END REPeat Bucle_Lectura
2700     RETurn FALSE
2710     :
2720 END DEFine LeerFichero

Esta es la función de lectura del fichero de origen, como el final de las líneas puede ser LF, CR+LF o solo CR dependiendo del origen del fichero, no puedo leer por líneas sino que leo carácter a carácter, y si no es un CR o un LF lo incluyo en la línea que estoy leyendo, pero si es uno de estos entiendo que es el fin de línea y retorno lo que he leído hasta el momento. Si el siguiente carácter es CR o LF, es de la línea anterior o es una línea en blanco, lo que no está realmente permitido, y en este caso no retorno nada y sigo leyendo.

Ahora el procedimiento para guardar las línea convertidas en el fichero de salida, como vemos es muy  sencillo ya que guarda la línea completa sin complicaciones, en formato del QL que es el que lo ejecutará:

2760 DEFine PROCedure Guardar(s$)
2770   PRINT #cOut, s$
2780 END DEFine 
 

Como de momento el proceso solo copia tal cual, si se ejecuta con un fichero de prueba la pantalla que aparece será como esta:

 

Vemos que se solicita los nombres de los ficheros de origen y destino, y el proceso va presentando las líneas, el primer número en color blanco es el contador de líneas leídas. Luego en verde está la línea que ha leído, marcada como "L:", debajo y en color rojo la línea que ha convertido (repito que nuevamente en esta fase solo es copia de la leída) marcada como "C:".

El módulo completo lo podéis descargar desde este enlace

Thursday, 05. September 2024

Programación del Sinclair QL (XXV): Arboles. Pruebas de los nodos

11:39 GMT (over a year ago)

Índice de entradas sobre programación del Sinclair QL



Vamos con el programa para poder probar lo escrito en la entrada anterior. Es muy sencillo, genero en un bucle una cantidad aleatoria de nodos, cada uno con un valor que será una cadena de caracteres aleatorios entre "A" y "Z". Luego pruebo las rutinas de modificar los punteros a los hijos de los nodos, y la de cambiar el contenido del nodo.

Como las rutinas para montar los nodos están en otro fichero, tengo que cargarlo con un MERGE. En las entradas anteriores usaba un módulo base con los MERGE, y si se ejecutaba el módulo más de una vez volvía a ejecutarlos, con la pérdida de tiempo que eso genera.

La solución ha sido añadir un controlador de errores, si detecta que no se ha cargado el módulo hace el MERGE, si ve que se ha cargado no hace nada. De esta forma podemos ejecutarlo varias veces sin pérdidas de tiempo.

  • WHEN ERRor: Es la rutina que se encarga de errores, para activarla se mira en la línea 260 el valor de una variable NODOS, si no existe dará un error -17
  • Prepara el sistema: Prepara la pantalla y llama a la rutina del módulo de nodos para inicializarlo
  • Crea módulos: Crea los nodos con información aleatoria
  • Modifica módulo: Modifica un nodo para añadir puntero a los hijos y cambia el contenido.
  • RndChr: Retorna un caracter aleatorio entre "A" y "Z"


100 REMark *************************************************************
110 REMark * Pruebas para el Manejo de nodos del arbol binario
120 REMark *************************************************************
130 :
140 REMark --- Control de si se ha incluido o no el módulo de nodos
150 :
160 WHEN ERRor
170   IF ERNUM=-17 THEN
180     PRINT "Cargando rutinas de nodos..."
190     MERGE mdv1_nodos
200     NODOS=1
201     RETRY
210   ELSE
220     PRINT "Se ha producido un error ";ERNUM;" en l“nea ";ERLIN
230     STOP
240   END IF
250 END WHEN
260 IF NODOS=0 THEN REMark --- Si da un error es que no ha cargado ---
270 :
280 REMark --- Preparar el sistema
290 :
300 MODE 0 : PAPER 0 : CLS
310 inicializa
320 :
330 REMark --- Generar nodos aleatorios para probar las rutinas
340 :
350 generar = RND(1 TO 10) : PRINT "Generando "& generar & " nodos"
360 FOR n=1 TO generar
370   c$=""
380   IF RND(1 TO 9) > 2 THEN
390     FOR i=0 TO RND(20)
400       c$ = c$ & RndChr$
410     NEXT i
420   END IF
430   n$ = NuevoNodo$(n,c$)
440   PRINT n$
450 NEXT n
460 :
470 PRINT "Finalizados los ";n;" nodos"
480 PRINT
490 PRINT "Cambiando datos de un nodo"
500 PRINT NuevoHijo$(n$,pIzq,56)
510 PRINT NuevoHijo$(n$,pDer,67)
520 PRINT NuevoValor$(n$,"Pruebas de valores")
530 :
540 REMark --- Retorna un caracter aleatório entre A y Z
550 :
560 DEFine FuNction RndChr$
570   RETurn CHR$(RND(CODE("A") TO CODE("Z")))
580 END DEFine aleatorio
590 :

 

El programa fuente lo podéis bajar de aquí.


En la siguiente entrada se añadirá el módulo memoria, añadiendo y eliminando nodos en la misma, por lo que luego mejoraré este programa para poder hacer pruebas de ambas partes, añadiendo un menú para definir cual se desea probar.

Para completar habría que hacer un proceso para compactar la memoria, pero eso requiere cambiar los punteros de los nodos, para lo que es necesario conocer su estructura interna, cosa que el sistema operativo no puede hacer, es responsabilidad de nuestro programa. En los lenguajes modernos con recolectores de basura, como Java o C# intentan hacer esto, con la ventaja de que no usan punteros a memoria, sino que las referencias son a la tabla de variables, con lo cual al cambiar la tabla ya ha cambiado todos los punteros, lo que es mucho mas sencillo que ir buscando en cada objeto a donde apunta y cambiarlo, pero a cambio usan mas memoria para la tabla de variables.



Programación del Sinclair QL (XXII): Variables

06:34 GMT (over a year ago)

Ampliado el 04/09/2024 en azul y el 05/09/24 en rojo


Retomo tras varios años la serie de programación de mi querido QL, seguiremos con los árboles, pero antes hablemos de conceptos básicos sobre punteros, necesarios para entender como montar los árboles.

Aunque esta entrada entrá en la serie del QL, realmente es general sobre programación, debes entender estos conceptos ya que son válidos para cualquier lenguaje, y en la siguiente hablaré sobre punteros.

Compiladores e Intérpretes

Escribimos nuestros programas en algún lenguaje, en donde usamos variables como un sistema para almacenar los datos que manipulemos y la lógica que usamos para su manipulación. Para ejecutar el programa podemos usar dos sistemas:

  • En BASIC normalmente se usa un interprete, que va leyendo el programa fuente línea a línea y ejecutándola, esto es bastante simple y necesita poco espacio en memoria.
  • Los compiladores en cambio convierte el programa fuente en otro que reconozca la máquina (normalmente código máquina, pero en algunos como JAVA generan un código intermedio que luego se procesan usando un ejecutor o una máquina virtual). Para ello se usa un programa que va leyendo el programa y ejecutando el proceso en una o varias pasadas, montando una serie de tablas para poder manejar cosas que todavía no se han definido. Esto requiere mas memoria para disponer del compilador, del buffer donde almacenar el programa que vamos leyendo, las tablas auxiliares, etc.

Variables

En ambos casos se necesita reconocer las variables, de forma que siempre que encontremos una podamos usar su valor correcto. Para ello en ambos sistemas se crea una tabla de variables, en donde se almacenan los datos necesarios para manejarlas, lo que se denominan atributos. En esta tabla se incluyen varios atributos importantes:

  • Nombre: El nombre que le damos a una variables para nosotros es su principal atributo, ya que es con el que interactuamos con ellas. Su longitud es muy variable entre lenguajes, en los primeros BASIC constaba de una letra o de una letra y un solo dígito (A, B$, C2, D4 ...), en los TRS-80 su BASIC permitía nombre largos, pero solo consideraba los dos primeros (lo que solo es un poco mejor), y por experiencia personal se puede programar así solo requiere disciplina. En lenguajes antiguos se comenzaron usando hasta 6, luego se aumentó a 8 (bastantes años más tarde, y en las primeras versiones del ABAL, un dialecto del BASIC usado por BULL, se mantenían ilimitados pero solo consideraba los 8 primeros), rápidamente creció el tamaño hasta 256, y hoy día podemos decir que los lenguajes modernos tienen longitud ilimitada. Los nombres deben comenzar por una letra, o por el símbolo de subrayado más una letra. Hay lenguajes que distinguen mayúsculas y minúsculas en los nombre (lo que ocasiona errores si te equivocas, por lo que se recomienda usar siempre mayúsculas o minúsculas).
  • Tipo de Contenido: En los lenguajes antiguos teníamos variables numéricas de tipo entero o de tipo decimal, y variables alfanuméricas, de manera similar a lo que soportan los propios ordenadores internamente, luego se han ido ampliando los tipos:
    • Alfanuméricas: Almacenan caracteres alfabéticos, caracteres numéricos y símbolos especiales (espacio, punto, +, *, &, etc.). En general hay estos tipos:
      • Carácter o char: Un solo caracter
      • Cadena o string: Una lista de caracteres, que puede ser de longitud fija (tienen un máximo que no se puede sobrepasar, hay lenguajes que rellenan con espacios por la derecha y otros que no lo hacen, marcando el final de la cadena), o de longitud variable (la cadena ocupa su longitud al crearla, si se amplia su contenido se amplia la cadena a su vez).
En el QL las variables de cadena se definen añadiendo el símbolo $ al nombre de la variable, y almacenan valores de 8 bits en código ASCII, aunque dependiendo de la ROM usará una configuración regional diferentes para algunos caracteres locales, como la Ñ o la £.
    • Enteras: Un valor numérico sin decimales, son los más rápidos de manejo por lo que debe priorizarse su manejo. Internamente se guardan en binario, usando o bien un bit para el signo o bien usando complementos (esto lo explicaré mas adelante), su representación interna siempre es exacta, pero están limitados en rango por su tamaño, así en 8 bits y usando uno para el signo, disponemos de valores entre -127 a +127 (realmente -128 a 127, cosas del binario, pasará lo mismo con el resto de tamaños), con 16 bits ampliamos a ±32.767, con 32 bits alcanzamos ±2.147.483.647, con 64 bits ±9.223.372.036.854.775.808 (ojo, el punto es el separador de miles que usamos en España, en otros países serían comas). Realmente hay que
El tamaño del entero es el base del procesador, en máquinas antiguas serían 8 bits, hoy día es de 64, y se han definido muchas variantes:
      • Entero: Por defecto del tamaño de registro del procesador (8,16,32,64)
      • Entero sin signo: Si no necesitamos números negativos, con este tipo ampliamos en 1 bit el tamaño, por lo que con 8 bits serían de 0 a 256, con 16 bits de 0 a  65.536, etc.
      • Entero corto y largo: Cuando las máquinas eran de 8 bits no había otra opción, pero al ir ampliando el tamaño a 16, para reducir ocupación en memoria que era un recurso muy limitado siempre, se definieron enteros cortos que ocupaban la mitad, y enteros largos que ocupaban el doble.
      • Carácter: Representa el valor numérico de un carácter alfanumérico en el código ASCII, pero en ciertos lenguajes podemos usarlo como un entero corto de 8 bits (o 16 si es en Unicode), soportando también su uso con o sin signo. 
      • Booleanas: Un entero en donde 0=Falso y 1=Cierto, o más generalmente cero falso y diferente a cero cierto. Ocupan un byte completo para solo usar un bit, lo que en máquinas de 64 bits es mucho espacio desaprovechado.
      • Fecha, hora, fecha y hora: Son valores enteros que guardan realmente el número de días desde una fecha inicial dada, la hora del día en décimas de segundo, o ambos simultáneamente en dos enteros.
      • Punteros: Esto lo veremos luego.
      • De coma fija: Estos son números que tienen una cantidad de decimales predefinida, y su valor se guarda directamente en binario como un entero, redondeando el valor a los decimales que aceptan, ajustando los decimales truncando o añadiendo ceros por la derecha para alcanzar los definidos, y luego eliminando el punto decimal, así serán enteros y de esta manera se pueden sumar directamente entre sí, añadiendo ceros por la derecha si es necesario igualar el número de los decimales entre las variables (esto es muy rápido en los ordenadores).
En el QL las variables enteras se definen añadiendo el símbolo % al nombre de la variable, y almacenan valores en 16 bits, entre -32.768 y +32.767.
    • Decimales: Un valor numérico que admite decimales. Internamente se guarda con una codificación científica, del tipo smEn, donde s es el signo, m la mantisa (se guarda el valor completo como todos sus dígitos sin punto decimal), y n sería el exponente (donde se ubica el signo decimal). Hay varios tipos según su tamaño, lo que cambia es la cantidad de cifras significativas de la mantisa y del exponente.:
      • La codificación normalizada IEEE 754 ocupa 32 bits, denominándose de simple precisión
      • Las de media precisión ocupan 16 bits
      • Las de doble precisión ocupan 64 bits
      • Las de cuádruple precisión o doble precisión largas, con 128 bits
      • Hay hasta las de octuple precisión con 256 bits.
En el QL las variables decimales se definen sin añadir nada a su nombre, y almacenan valores en 16 bits, su valor se presentará como un número si no es excesivamente grande, o con formato científico en caso contrario. 
    • Tipos especiales: Aquí cada lenguaje tiene sus variantes, podemos hablar en general de:
      • Puntero:  que guarda una posición de memoria, es el objetivo de esta entrada
      • Tipo indefinido (void): representa que no usaremos su valor por lo que solo nos interesa su nombre (en C una función se comporta como una variable, ya que dispone de su entrada en la tabla de Variables, las funciones son las que retornan un valor, los procedimientos no las retornan y se definen de tipo void)
      • Cualquier tipo (any): que indica que guardaremos cualquier posible valor en la variable (usar con cuidado ya que ralentiza mucho el programa). Son usuales en JavaScript.
      • Objeto: realmente son punteros, pero que apuntan a una estructura en memoria compleja que dispone de código y datos.
El tipo de contenido representa un tamaño en la memoria. Hay lenguajes fuertemente tipados, que no permiten asignar una variable de un tipo a una de otro tipo, y lenguajes pobremente tipados que hacen conversiones automáticas entre los tipos de las variables (esto por si solo requiere una entrada para ello, ya veremos si algún día la hago). NOTA: Nuestro QL es pobremente tipado, por tanto podemos asignar a una numérica una cadena, e intentará hacer la conversión a partir de su contenido. Si escribís a$="12.45 MAS" : b=a$ : c%=a$ : PRINT a$, b, c% veréis que en pantalla aparece   12.45 MAS       12.45       12

  • Ubicación en memoria: Todos los datos deben estar ubicados en algún lugar de la memoria del ordenador, en general se reserva una zona denominada "pila del programa" o de forma similar, en donde se guardan todas las variables que se utilicen. Para variables simples apuntará directamente a donde está su valor, pero para variables mas complejas apunta al inicio de la zona en que está ubicada. Para crear la entrada para un nuevo valor, se mira la posición de la última entrada y se le suma la ocupación, así tenemos la ubicación de la nueva variable. Si fuera la primera variable y por tanto la tabla está vacía, se rellena con la dirección base de la "pila del programa".
Desde nuestro programa no vemos esa tabla de variables ni sabemos nada de direcciones de memoria, realmente solo nos interesa conocer el VALOR que es el contenido de la variable, realmente unas posiciones concretas en la memoria apuntadas por la tabla anterior. En la mayoría de lenguajes su valor se inicializa al comenzar el programa a un valor por defecto, cero para numéricas (en booleanas cero equivale a false, en fechas el valor sería 0, pero como representa los días que transcurren desde una fecha base preestablecida sería el valor de esa fecha), las cadenas se inicializan como vacías o con espacios según el lenguaje.
 
Hay que tener cuidado con lenguajes donde las variables no se inicializan como C o C++ , tendrán un valor aleatorio según lo que haya en ese momento en la memoria, por lo que se siempre se recomienda darles el valor inicial al definirlas en la forma int a=0; float b=0.0; char Saludo[5]="Hola";
 
En nuestro QL el sistema utiliza tres listas separadas para el manejo de las variables, procedimientos y sus nombres:
  • Por un lado tenemos la lista de nombres de las variables, procedimientos y funciones. La primera vez que las definimos se guarda en esta lista, y a partir de entonces al escribir su nombre el sistema lo ajustará para que coincida con lo que has escrito en cuanto a mayúsculas y minúsculas en las líneas. Esto hace que si queremos cambiar la variable nododerecho por NodoDerecho el sistema no nos deje, pues ya está en la lista.
  • Otra lista contiene los tipos de cada variable, procedimiento o función, para las locales incluye el procedimiento o función en que tienen vigencia, junto al puntero en la memoria donde está ubicado su valor.
  • Una tercera es la que contiene los valores de las variables.

Definición explicita de variables

En BASIC o Python no es necesario definir las variables antes de su uso, son lo que se denomina lenguajes implícitos, conforme avanza el programa se va rellenando la tabla de variables cuando se localiza una nueva, esto puede ocasionar problemas ya que si nos equivocamos en el nombre, el sistema crea una nueva variable sin que nos demos cuenta, lo que provocará un error que habitualmente cuesta localizar.
 
En contra la mayoría de lenguajes son de tipo explícito, obligan a definir una variable antes de poder usarla, algunos como en COBOL separadas del código ejecutable, otros como Pascal C (y los que usan su sintaxis como C++, C#, Java o PHP) se pueden definir en cualquier momento. Al definir las variables, si nos equivocamos en el nombre dará un error por variable no definida.

Hay algunos lenguajes que se pueden configurar por la configuración del entorno de programación o usando una instrucción de pre-procesador en el propio programa para que se comporten de una manera o de la otra.

Variables globales y variables locales

En el BASIC estándar todas las variables son globales, eso significa que las podemos usar en cualquier parte del programa sin problema. Evidentemente en lenguajes donde definimos las variables antes de comenzar el programa en un bloque separado, como COBOL o en el ABAL de Bull, todas las variables son también globales.
 
En lenguajes que soportan funciones o procedimientos, existe la posibilidad de definir variables que solo se verán dentro de ellas, desapareciendo su valor al salir de las mismas (aunque en algunos se conservan los valores, perdiendo entonces parte de su ventaja). Estas son variables locales.

La idea de las locales surgió en ALGOL, donde existen bloques del programa, estos bloques se marcan con una instrucción de inicio y otra de fin, por ejemplo en C se usan las llaves {} mientras en PASCAL se usa begin y end. Dentro de estos bloques también podemos definir variables locales a los mismos.

Una variable local pueden usar el mismo nombre que una global, siendo ambas independientes, en la función se usará la variable local, y fuera de ella se usará la global, pero esto puede dar lugar a errores dentro de nuestras funciones por pensar que estamos usando la global, por eso hay que ser cuidadosos en el nombre de las variables. Para evitar errores es buena costumbre usar una letra antes del nombre, sobre todo en las locales, por ejemplo añadir loc delante.
 
Un uso muy habitual de una variable local en C es para los bucles definiendo las variables índice dentro del propio bloque, o incluso en la definición del mismo, por ejemplo for(int i=1;i<10;i++) { ... } esto es un bucle en donde i tendrá los valores del 1 al 9, y después del mismo desaparece. Veamos un ejemplo un poco mas completo:

(1)    int i=84;
(2)    do {
(3)        int i=6;
(4)        i++;
(5)        printf("%d", i);
(6)    } while (i<10) 
(7)    printf("%d", i);
 
La línea 5 presentará los valores de la variable i que es local al bucle: 6,7,8,9 y luego en la línea 7 presentará la variable global con valor 84.
 
En nuestro querido SuperBASIC se permie definir variables locales en las funciones y procedimientos, usando el comando LOC seguido de las variables que queramos definir. Por ejemplo
 
10 a=1 : b$="A"
20 print "1",a,b$
30 prueba
40 print "3",a,b$
50 Define PROCedure prueba
60     loc a,b$
70     a=5 : b$="U"
80    print "2",a,b$
90 End DEFine 
 
Al ejecutarlo imprimirá:
1   1   A
2   5   U
3   1   A 

En la siguiente entrada abordamos los punteros, de uso muy habitual en C, pero fuente de problemas por lo que se prefiere ocultados en otros lenguajes como Java, que los usan implícitamente sin que los veamos.



Programación del Sinclair QL (XXIV): Arboles. Estructura de los nodos en Super Basic

06:24 GMT (over a year ago)

 

Índice de entradas sobre programación del Sinclair QL



Hemos visto el tema de las variables y el de los punteros, ahora vamos a hacer algo en Super Basic, pero ya que no es un lenguaje que soporte punteros ni se pueden usar variables compuestas de tipo registro, es un poco más complicado su manejo. Para manejar memoria hay que usar llamadas al Sistema Operativo para reservar memoria y código máquina para manejar los registros y punteros, o en el Toolkit II hay algunas funciones que nos lo facilitan. Como esto se sale de estas entradas que son de programación en SuperBasic, con la idea de que se pueda portar a otros Basic, voy a hacer otra cosas diferente para manejar árboles binarios, aunque no descarto luego hacer algo usando código máquina y llamadas al sistema.

Simularé la memoria usando una cadena de caracteres, usando el hecho de que una cadena tiene n caracteres, y podemos acceder a cualquiera de ellos individualmente, y cada carácter tiene un valor de 8 bits, lo que es muy similar a como se maneja una memoria. Desarrollaré funciones para reservar "memoria" dentro de la cadena, guardar datos en ella, leer cualquier dato usando un puntero a su dirección base, etc.

Nodos

Para crear los nodos del árbol, en entradas anteriores usé varios vectores de enteros, ya que los valores que manejamos lo eran. Ahora usaremos valores de tipo cadena de longitud variable, por lo que ya no nos sirve usar un vector, desperdiciamos demasiada memoria definiendo cadenas de longitud fija que pueden estar casi vacías. En su lugar en C se usaría un registro, que es una variable que se subdivide en varias, comportándose como un todo o como cada variable individual. Veamos como lo definiríamos en C:

struct nombre_registro    //El nombre que deseamos darle
{
     tipodato campo_1;    //Se define como cualquier variable, tipo y nombre
     tipodato campo_2;
     ....
     tipodato campo_N;    //Podemos usar todas las variables necesarias       
};

En el programa usaremos el nombre como el tipo de variable:

nombre_registro Variable_Registro;     //El nombre que deseamos darle a la variable

Y luego llamamos a los valores añadiendo un punto al nombre:

Variable_Registro.campo_n = ....
if (Variable_Registro.campo_n == true) {....

En los árboles binarios, necesitamos estos datos dentro para montar el registro:

  • Nodo_Izquierdo: Puntero al sub-nodo izquierdo de este nodo.
  • Nodo_Derecho: Puntero al sub-nodo derecho de este nodo.
  • Nodo_Padre: Puntero al nodo padre de este nodo.Este dato no es estrictamente necesario, pero simplifica el montaje del árbol.
  • Contenido del nodo: En nuestro caso es una cadena de caracteres, pero puede ser cualquier tipo, como un entero o un vector, o incluso otra estructura de tipo registro.
  • Longitud del contenido: Al ser una cadena de longitud variable, necesitamos sabes su longitud para reservar solo la memoria necesaria.

Funciones para montar registros

Para montar un registro usaré una cadena de caracteres dividida en partes, los punteros serán valores numéricos entre 0 y 99, mientras que el contenido lo limitaremos a entre 0 y 99 caracteres. 

Para almacenar los números usaré cadenas de longitud fija, ajustadas a la izquierda con ceros por la derecha, así será sencillo ver su valor, realmente lo que se debe usar son bytes, con 1 byte podemos almacenar un número entero sin signo entre 0 y 255, con 2 bytes alcanzamos entre 0 y 65.535, esto pueden ser fácilmente dos caracteres, pero muchos no sería visibles ni nos informarían de los valores del puntero, por eso prefiero usar números almacenados en la cadena, aunque solo puedo almacenar entre 0 y 99 tengo la ventaja de que los puedo ver de forma sencilla, y este desarrollo intenta ser instructivo más que efectivo.

Nuestro nodos serán una cadena de caracteres con este formato <NN:NN:NN:NN:cadena> en donde:

  • Inicio del nodo: Un carácter "<"
  • Puntero al nodo padre: Dos caracteres numéricos. No es necesario pero simplifica la navegación por el árbol.
  • Separador: Un carácter ":"
  • Puntero al nodo hijo izquierdo: Dos caracteres numéricos.
  • Separador: Un carácter ":"
  • Puntero al nodo hijo derecho: Dos caracteres numéricos.
  • Separador: Un carácter ":"
  • Longitud del contenido: Dos caracteres numéricos.
  • Separador: Un carácter ":"
  • Contenido: Una cadena de entre 0 y 99 caracteres
  • Fin del registro: Un carácter ">"

Los caracteres de inicio y fin no son necesarios, así como los separadores tampoco lo son, pero los usaré para poder ver los nodos de manera sencilla, y rellenaré la "memoria" con asteriscos, de forma que se vean los huecos libres, y cuando queramos ver la "memoria" lo podemos hacer simplemente imprimiendo la cadena, y veremos un texto de la forma:

<00:01:23:04:HOLA>******************<19:40:00:05;ADIOS><04:25_09:00:>********....

Así creo que es mas sencillo presentar los valores de forma que se vean correctamente, sin necesidad de grandes procesos.

Empecemos por las funciones que montan los nodos, para lo que defino funciones y procedimientos para manejarlas:

  • inicializa: Este procedimiento inicializa las variables que usaremos, en donde:
    • mLon: Longitud de las cadenas para guardar los punteros
    • mNodos: Máximo número de nodos que manejamos
    • nIni, nSep, nFin: inicio, separador y final para montar el nodo
    • pPadre, pIzq, pDer, pLon, pVal: Orden de los campos para el nodo
  • n2c$(n,l): Esta función retorna una cadena cuyo contenido es un valor numérico (n), ajustada a una longitud máxima (l) y rellena con ceros por la izquierda. Si le pasamos por ejemplo (17,3) nos retorna "017"
  • MontarNodo$(p,i,d,c$): Función que retorna un nodo montado con los valores para el padre (p), hijo izquierdao (i), hijo derecho (d) y una cadena de caracteres (c$), si le pasamos por ejemplo (2,12,3,"Hola") nos retorna "<02:12:03:04:Hola>"
  • NuevoNodo$(p,c$): Función que retorna un nuevo nodo sin hijos, con los valores para el padre (p) y una cadena de caracteres (c$), si le pasamos por ejemplo (2,"Hola") nos retorna "<02:00:00:04:Hola>"
  • Posicion(pos): Retorna un número que indica donde comienza en la cadena que representa el nodo uno de los valores del nodo (pos con valores para 0=padre, 1=izquierdo, 2=derecho, 3=longitud del contenido, 4=valor del contenido), si le pasamos por ejemplo (pIzq) nos retorna 5
  • NuevoHijo$(nodo$, pos, valor): Retorna una cadena que representa un nodo (nodo$), en la que cambia el puntero para uno de los hijos (pos donde 1=izquierdo o 2=derecho), con su nuevo valor de puntero (valor), si le pasamos por ejemplo ("<02:00:00:04:Hola>", pIzq,2) nos retorna "<02:02:00:04:Hola>"
  • NuevoValor$(nodo$, NuevoValor$): Retorna una cadena que representa un nodo (nodo$), en la que cambia el valor del contenido (NuevoValor$), si le pasamos por ejemplo ("<02:02:00:04:Hola>", "Adios") nos retorna "<02:02:00:05:Adios>"

5000 REMark *************************************************************
5010 REMark * Librería.: Arbol Binario                                  *
5020 REMark * Programa.: Nodos                                          *
5030 REMark * Contenido: Rutinas de manejo de los nodos                 *
5040 REMark * Autor....: javu61, octubre 2024                           *
5050 REMark *************************************************************
5060 :
5070 REMark --- Inicializar el sistema
5080 :
5090 DEFine PROCedure inicializa
5100   mLon=2
5110   mNodos = 10^mLon - 1
5120   nIni$="<" : nSep$=":" : nFin$=">"
5130   pPadre=0 : pIzq=1 : pDer = 2 : pLon=3 : pVal=4
5140 END DEFine inicializa
5150 :
5160 REMark --- Numero en cadena ajustado a longitud
5170 :
5180 DEFine FuNction n2c$(n,l)
5190   LOCal c$
5200   c$=n
5210   REPeat bucle_n2c
5220     IF LEN(c$)>=l THEN EXIT bucle_n2c
5230     c$ = "0" & c$
5240   END REPeat bucle_n2c
5250   c$= c$(1 TO l)
5260   RETurn c$
5270 END DEFine n2c$
5280 :
5290 REMark --- Montar un nodo
5300 :
5310 DEFine FuNction MontarNodo$(p,i,d,c$)
5320   LOCal n$
5330   n$ = nIni$
5340   n$ = n$ & n2c$(p,mLon) & nSep$
5350   n$ = n$ & n2c$(i,mLon) & nSep$
5360   n$ = n$ & n2c$(d,mLon) & nSep$
5370   n$ = n$ & n2c$(LEN(c$),mLon) & nSep$ & c$
5380   n$ = n$ & nFin$
5390   RETurn n$
5400 END DEFine MontarNodo
5410 :
5420 REMark --- Montar un nodo nuevo sin hijos
5430 :
5440 DEFine FuNction NuevoNodo$(p,c$)
5450   RETurn MontarNodo$(p, 0, 0, c$)
5460 END DEFine NuevoNodo
5470 :
5480 REMark --- Retorna la posición de un elemento en el nodo
5490 :
5500 DEFine FuNction Posicion(pos)
5510   RETurn LEN(nIni$) + (pos * mLon) + (pos * LEN(nSep$)) + 1
5520 END DEFine Posicion
5530 :
5540 REMark --- Añadir un hijo a un nodo
5550 :
5560 DEFine FuNction NuevoHijo$(nodo$, pos, valor)
5570   LOCal p
5580   p = Posicion(pos)
5590   nodo$(p TO p+mLon-1)= n2c$(valor, mLon)
5600   RETurn nodo$
5610 END DEFine NuevoHijo$
5620 :
5630 REMark --- Cambiar el valor de un nodo
5640 :
5650 DEFine FuNction NuevoValor$(nodo$, NuevoValor$)
5660   LOCal p1,p2
5670   p1 = Posicion(pLon)
5680   p2 = Posicion(pVal)
5690   nodo$ = nodo$(1 TO p1-1)
5700   nodo$ = nodo$& n2c$(LEN(NuevoValor$), mLon) & nSep$
5710   nodo$ = nodo$ & NuevoValor$ & nFin$
5720   RETurn nodo$
5730 END DEFine NuevoValor$

 

En la siguiente entrada añadiré un programa para probar que estas funciones trabajen correctamente, y si queréis descargar este fichero lo tenéis aquí.

Thursday, 08. August 2024

Programación del Sinclair QL (XXIII): Punteros

08:30 GMT (over a year ago)

Índice de entradas sobre programación del Sinclair QL



Tras la entrada sobre las variables, entraremos a explicar lo que son los punteros, ya que la definición habitual es real pero muy poco explicativa: Un puntero es una variable que apunta a otra variable.

En la siguiente entrada usaremos ya los punteros en SuperBasic, que aunque no los soporta de forma nativa, hay maneras de simularlos.

Punteros

Como vimos en la entrada anterior, los compiladores e intérpretes usan una tabla en donde almacenar los datos de las variables que se utilizan en el programa. Las entradas de esta tabla disponen de una serie de datos importantes: nombre, tipo, longitud y posición en memoria donde se almacena (lo que se denomina dirección base de la misma, y su ocupación es la longitud de la misma (normalmente viene dados por el tipo).

Un puntero es una variable de tipo entero, que almacena una dirección de memoria, usualmente la dirección base de otra variable o de una zona de memoria libre. Veamos los dos tipos con ejemplos en  lenguaje C (lo tengo bastantes oxidado, quizá no sea del todo correcta la sintaxis): 


[1] float a=78.65;
[2] float *b=&a;
[3] float *c=malloc(4);
[4] printf('%d %f', b, *b);
[5] printf('%d %f', c, *c);
[6] *c = a;
[7] printf('%f', c);


  • En la línea 1 creamos una variable de tipo decimal que ocupa 4 octetos, y le damos como valor inicial 78'65. 
  • En la línea 2 creamos una variable de tipo puntero, que apuntará a una zona de memoria que contiene una variable de tipo decimal de 4 octetos, y la inicializamos con la dirección de base de la variable a. 
  • En la línea 3 creamos una variable de tipo puntero que apuntará a una variable de tipo decimal, y lo inicializamos a una nueva zona de memoria en donde se reservan 4 octetos.
  • En la línea 4 el sistema imprimirá la dirección de memoria a la que apunta la variable b, seguida por el contenido de la memoria a la que apunta b, en este caso 78'65.
  • En la línea 5 el sistema imprimirá la dirección de memoria a la que apunta la variable c, seguida del contenido de esa dirección de memoria, en este caso indefinido pues no se ha inicializado a ningún valor, puede ser cero o dar un error por no servir el contenido como representación de una variable decimal.
  • En la línea 6 asociamos a la dirección base apuntada por la variable c el contenido de la variable a, sería lo mismo que hacer un c=a si ambas variables fueran float.
  • En la línea 7 se imprime 78'65

Como vemos, si una variable de tipo puntero la usamos por su nombre, obtenemos la dirección de memoria a la que apunta, pero si el nombre lo precedemos por asterisco, obtenemos el contenido de dicha dirección de memoria. Igualmente una variable precedida por & nos retorna al dirección de memoria de dicha variable y no su valor.

Uso de los punteros a variables

Al definirse las funciones, los parámetros que les pasamos son variables locales cuyo valor inicial es que que les pasemos en la llamada, y al finalizar  solo retornan un valor, pero en C definieron un sistema para poder alterar los parámetros  usando los punteros. Vemos un ejemplo:


[1] void incrementar(int i) { i = i + 1; }
[2] void duplicar(int *j) { j = j + 1; }
[3] int a=5; incrementar(a); printf('%c', a);
[4] int b=5; duplicar(&b); printf('%c', b);

  • En la línea 1 creamos una función que recibe un parámetro, que es una variable local a la misma, esto incrementa la variable en una unidad. En la línea 2 definimos una semejante, pero recibe un puntero como parámetro.
  • En la línea 3 definimos una variable entera con valor 5, llamamos a incrementar, al que le pasamos el valor de a que es 5, como en la línea 1 estamos incrementando la variable local no se afecta, y luego se imprime otra vez 5. 
  • En la línea 4 definimos una variable entera con valor 5, llamamos a duplicar, al que le pasamos la dirección de b, como en la línea 2 estamos recibiendo el puntero, al incrementar será la zona de memoria a la que apunte, por tanto al final de la línea imprimirá 6.

De esta manera es sencillo para el programador decidir que variables serán alterada en las funciones y cuales no, protegiendo mejor el programa. En lenguajes modernos sin punteros, se añade una palabra, normalmente var, antes de la variable para indicar que su valor podrá ser alterado, lo que internamente es usar un puntero de forma transparente.

Otro uso habitual es emplear punteros a funciones, de esta manera en una variable podemos guardar una u otra función en relación con los datos que estamos procesando, pudiendo alterar el manejo del programa sin necesidad de complicar mucho la lógica. 

En lenguajes modernos esto es transparente, por ejemplo un objeto es solo un puntero a una posición de memoria que contiene funciones y variables, lo manejamos sin necesidad de saber siquiera lo que es un puntero.

Uso de punteros a memoria

La principal ventaja de los punteros es el manejo de memoria libre de forma no estructurada y de longitud variable, pudiendo crear elementos libremente cuando los necesitemos, a diferencia de los vectores que debemos definir su número de elementos y el tamaño de estos antes de empezar a manejarlos. Esto es muy útil para manejar estructuras de datos, como una lista de cadenas, cuando necesitamos un elemento llamamos a una función que nos retorne una dirección de memoria libre, del tamaño que necesitemos para esa cadena más el puntero al siguiente elemento, guardamos los datos, ajustamos los punteros, y ya tenemos una lista que podemos recorrer, sin saber cuanto ocuparán las cadenas de caracteres ni cuantas usaremos. 

Esto lo hacen los lenguajes modernos de forma transparente, al crear una estructura de tipo lista, que luego podemos recorrer con un FOREACH.

Problemas de los punteros

Tras muchos años manejando punteros, por los problemas que pueden ocasionar su uso, en los lenguajes actuales se ha optado por eliminarlos, aunque el sistema los maneja de forma interna de forma transparente, en nuestro programa no podemos usarlos.

El principal problema de los punteros es que son propensos a usarse de forma errónea provocando errores en el programa o, lo que es peor, en el sistema. Si apunto mal en una lista, puedo crear una circular y no salir nunca del bucle que la recorra, o apuntar mal un elemento y salirnos de la lista, por lo que el programa trabajará con datos erróneos. En un vector no podemos usar mas elementos de los definidos, pero con punteros podemos pasarnos y seguir leyendo elementos incorrectos. Esto se soluciona siendo cuidadosos al programar y probar bien nuestros programas con muchos datos diferentes.

Otro problema es la fragmentación, si creamos y destruimos muchos trozos pequeños de memoria, el sistema no puede reutilizarlos y podemos ocupar toda la memoria posible sin darnos cuenta. Esto tiene difícil solución, habría que compactar la memoria reubicando los registros y los punteros, lo que puede ser largo. Esto es realmente lo que hacen en lenguajes modernos los recolectores de basura de forma automática y transparente, ocultando los punteros a los programadores.

Otro problema es la seguridad, mediante un puntero podemos acceder a cualquier posición de memoria, por tanto podemos leer en otras zonas de memoria que no son específicas de nuestro programa, en sistemas como UNIX que son multi-usuario, podemos leer la memoria asignada a otro usuario y obtener por ejemplo claves de acceso. Hay mecanismos en el sistema operativo para que esto no sea posible, dando errores si accedemos a ubicaciones de memoria que no son del rango que tenemos asignado a nuestro programa.

Tuesday, 16. July 2024

Programación del Sinclair QL (XIX): Estructura de datos. Arbol binario de Búsqueda

09:03 GMT (over a year ago)

<0 hay="" libres="" no="">Errara corregida el 16/07/24 en la línea 3050


Los arboles son las estructuras mas usadas para almacenar datos, ya que permiten un muy rápido acceso a ellos. La estructura en sencilla, un registro se divide en dos partes, una con los datos que se necesitan guardar y otro con enlaces a los elementos hijos, que será un número mayor o igual a dos.

Ejemplo de arbol (Fuente: Monografias.com)


Si el número máximo de hijos es dos se denominan árboles binarios, y son los mas usados en programación en general, reservando los árboles de mas ramas para uso en ficheros indexados habitualmente. No voy a explicar mucho mas, hay suficiente información en la Web sobre arboles y su manejo.

Existe un tipo especial de árbol binario, en el que en cada nodo de cada rama, los valores de los elementos que cuelgan del nodo izquierdo son todos menores al valor del propio nodo, y los elementos que cuelgan del nodo derecho son todos valores mayores al del propio nodo, de forma que si recorremos el árbol primero la rama izquierda, luego el nodo, y luego la rama derecha, obtenemos una lista de elementos ordenados en orden creciente. Si la recorremos primero la rama derecha, luego el nodo y después la rama izquierda obtenemos la lista ordenada en forma decreciente. Este tipo de árboles se llaman "Árbol Binario de Búsqueda", y es lo que voy a implementar, ya que estamos hablando de algoritmos de ordenación. 

Voy a usar tres arreglos para almacenar los datos, en uno me guardo los datos del registro (en este caso será una lista de números, pero puede ser cambiado de manera sencilla a una lista de cadenas por ejemplo), otro arreglo numérico con los enlaces a los punteros de la rama izquierda, y un tercer arreglo con los enlaces a la rama derecha. Estos dos arreglos podrían ser uno solo, pero creo que así está un poco mas claro.

Usaré el arreglo de enlaces a nodos izquierdos para almacenar la lista de elementos libres, pero por que sea mas visual haré lo mismo con los derechos. Esto lo cambiaré mas adelante por algo un poco mejor, ya lo veremos. El manejo de los árboles requiere muchos procedimientos recursivos, aunque usaré iterativos cuando sea posible para que se vean varias técnicas de manejo.

Uso una variable para almacenar el nivel máximo de profundidad del árbol, no tiene uso mas que para enseñarlo en pantalla, pero me permite que se vea algo que deseo demostrar luego. No me voy a complicar mucho cuando compacto el árbol, ya que esta parte la voy a cambiar luego, solo lo pongo de momento para que se vea.

2000 REMark --------------------------------------------------------------
2010 REMark -- ESTRUCTURAS DE DATOS                                     --
2020 REMark --   Modulo....: Arbol binario                              --
2030 REMark --   Objetivo..: Arbol binario usando un arreglo de valores --
2040 REMark --               numericos y dos para los punteros          --
2050 REMark --   Autor.....: javu61, 12/2016                            --
2060 REMark --   Procesos..:                                            --
2070 REMark --     ARBOL_CREAR      -- Crear el arbol                   --
2080 REMark --     ARBOL_NUEVO(tam) -- Crea el arbol con un tama‰o      --
2090 REMark --     b=ARBOL_LEN      -- Nro de elementos del arbol       --
2100 REMark --     b=ARBOL_NIVELES  -- Nro de niveles del arbol         --
2110 REMark --     b=ARBOL_BUSCAR(n)-- Busca un elemento en el arbol    --
2120 REMark --     ARBOL_ADD(dato)  -- Introduce un dato en el arbol    --
2130 REMark --     ARBOL_DELETE(n)  -- Borra el elemento n del arbol    --
2140 REMark --     v$=ARBOL_VER$    -- Muestra los elementos del arbol  --
2150 REMark --     ARBOL_CAMBIA(tam)-- Cambia el tama‰o del arbol       --
2160 REMark --   Procesos de uso interno:                               --
2170 REMark --     ARBOL_INIT       -- Inicializa una arbol             --
2180 REMark --     b=ARBOL_EL_LIBRE -- Retorna elemento libre           --
2190 REMark --     n=ARBOL_NIVELES  -- Retorna nro de niveles del arbol --
2200 REMark --   Elementos.: Se usan los siguientes elementos globales  --
2210 REMark --     A_TAM           -- Tama‰o del arreglo                --
2220 REMark --     A_FACTOR        -- Factor de crecimiento             --
2230 REMark --     ArrArbol(tam)   -- Elementos del arbol               --
2240 REMark --     ArrIzq(tam)     -- Puntero al elemento izquierdo     --
2250 REMark --     ArrDer(tam)     -- Puntero al elemento derecho       --
2260 REMark --     P_NULO          -- Puntero nulo                      --
2270 REMark --     P_VACIO         -- Puntero vacio                     --
2280 REMark --     A_RAIZ          -- Puntero al nodo raiz              --
2290 REMark --     A_NRO           -- Nro de elementos del arbol        --
2300 REMark --     A_NIVELES       -- Nro de niveles del arbol          --
2310 REMark --------------------------------------------------------------
2320 :
2330 :
2340 REMark --------------------------------------------------------------
2350 REMark -- ARBOL_CREAR -- Crea el arbol                             --
2360 REMark --------------------------------------------------------------
2370 DEFine PROCedure ARBOL_CREAR
2380   REMark Espacio para 10 elementos, crece minimo 5
2390   ARBOL_NUEVO 9,5
2400 END DEFine 
2410 :
2420 REMark --------------------------------------------------------------
2430 REMark -- ARBOL_NUEVO(tam,fac) -- Crea el arbol con un tama‰o y un --
2440 REMark --                         factor de crecimiento            --
2450 REMark --------------------------------------------------------------
2460 DEFine PROCedure ARBOL_NUEVO(tam,fac)
2470   LOCal i
2480   :
2490   LET P_NULO   = -1        : REMark Puntero nulo
2500   LET P_VACIO  = -2        : REMark Puntero vacio
2510   LET A_RAIZ   = P_NULO    : REMark Puntero al nodo raiz
2520   LET A_NIVELES= 0         : REMark Nro de niveles del arbol
2530   LET A_NRO    = 0         : REMark Nro de elementos del arbol
2540   LET A_TAM    = tam       : REMark Tama‰o del arreglo
2550   LET A_FACTOR = fac       : REMark Factor de crecimiento
2560   DIM ArrIzq(tam)          : REMark Punteros al elemento izquierdo
2570   DIM ArrDer(tam)          : REMark Punteros al elemento derecho
2580   DIM ArrArbol(tam)        : REMark Elementos del arbol
2590   :
2600   REMark Inicializa arbol de elementos libres
2610   FOR i=0 TO A_TAM : ArrIzq(i) = P_VACIO : ArrDer(i) = P_VACIO
2620 END DEFine 
2630 :
2640 REMark --------------------------------------------------------------
2650 REMark -- b = ARBOL_LEN -- Retorna el nro de elementos del arbol --
2660 REMark -- RETORNA: 0 si vacio, >0 = nro de elementos               --
2670 REMark --------------------------------------------------------------
2680 DEFine FuNction ARBOL_LEN
2690   RETurn A_NRO
2700 END DEFine 
2710 :
2720 REMark --------------------------------------------------------------
2730 REMark -- b = ARBOL_NIVELES -- Retorna el nro de niveles del arbol --
2740 REMark -- RETORNA: 0 si vacio, >0 = nro de elementos               --
2750 REMark --------------------------------------------------------------
2760 DEFine FuNction ARBOL_NIVELES
2770   RETurn A_NIVELES
2780 END DEFine 
2790 :
2800 REMark --------------------------------------------------------------
2810 REMark -- b = ARBOL_EL_LIBRE -- Retorna elemento libre del arbol   --
2820 REMark -- RETORNA:  <0 hay="" libres="" no="">=0 nro del elemento         --
2830 REMark --------------------------------------------------------------
2840 DEFine FuNction ARBOL_EL_LIBRE
2850   LOCal i
2860   :
2870   FOR i=0 TO A_TAM
2880     IF ArrIzq(i) = P_VACIO THEN RETurn i
2890   END FOR i
2900   RETurn P_NULO
2910 END DEFine 
2920 :
2930 REMark --------------------------------------------------------------
2940 REMark -- b = ARBOL_BUSCAR(valor) -- Busca un elemento en el arbol --
2950 REMark -- RETORNA:  nro del elemento o NULO si no encontrado       --
2960 REMark --------------------------------------------------------------
2970 DEFine FuNction ARBOL_BUSCAR(valor)
2980   LOCal nro,repetir
2990   :
3000   nro = A_RAIZ
3010   REPeat repetir
3020     IF nro = P_NULO THEN EXIT repetir
3030     IF ArrArbol(nro) = valor THEN EXIT repetir
3040     IF ArrArbol(nro) < valor THEN 
3050       nro = ArrIzq(nro)   : REMark errara corregida 16/07/24
3060     ELSE 
3070       nro = ArrDer(nro)
3080     END IF 
3090   END REPeat repetir
3100   RETurn nro
3110 END DEFine 
3120 :
3130 :
3140 REMark --------------------------------------------------------------
3150 REMark -- ARBOL_ADD(dato) -- Introduce un dato en el arbol, si     --
3160 REMark --                    esta llena crece al doble             --
3170 REMark --------------------------------------------------------------
3180 DEFine PROCedure ARBOL_ADD(dato)
3190   LOCal repetir,nro,pos,nivel
3200   :
3210   REMark Busco nodo libre, crece si hace falta y ubico el elemento
3220   REPeat repetir
3230     nro = ARBOL_EL_LIBRE
3240     IF nro <> P_NULO THEN EXIT repetir
3250     ARBOL_CAMBIA(A_TAM*2)
3260   END REPeat repetir
3270   ArrArbol(nro) = dato
3280   ArrIzq(nro) = P_NULO
3290   ArrDer(nro) = P_NULO
3300   nivel = 1
3310   :
3320   REMark Si es el primer nodo, lo ubico en la raiz
3330   IF A_RAIZ = P_NULO THEN 
3340     A_RAIZ = nro
3350   ELSE 
3360     REMark Si no es el primero, buscamos su posicion recorriendo
3370     pos = A_RAIZ
3380     REPeat repetir
3390       nivel = nivel + 1
3400       REMark Si es menor o igual pasamos a la izquierda
3410       IF (ArrArbol(pos) > dato) THEN 
3420         IF (ArrIzq(pos)=P_NULO) THEN 
3430           ArrIzq(pos) = nro
3440           EXIT repetir
3450         END IF 
3460         pos = ArrIzq(pos)
3470       ELSE 
3480         :
3490         REMark Si es mayor pasamos a la derecha
3500         IF (ArrDer(pos)=P_NULO) THEN 
3510           ArrDer(pos) = nro
3520           EXIT repetir
3530         END IF 
3540         pos = ArrDer(pos)
3550       END IF 
3560     END REPeat repetir
3570   END IF 
3580   :
3590   REMark Sumo un elemento al arbol y miro el nivel
3600   A_NRO = A_NRO + 1
3610   IF nivel > A_NIVELES THEN A_NIVELES = nivel
3620   :
3630 END DEFine 
3640 :
3650 REMark --------------------------------------------------------------
3660 REMark -- v$ = ARBOL_VER$ -- Muestra el arbol                      --
3670 REMark -- RETORNA: Lista de elementos del arbol                    --
3680 REMark --------------------------------------------------------------
3690 DEFine FuNction ARBOL_VER$(hastanivel)
3700   LOCal lista$
3710   :
3720   IF ARBOL_LEN = 0 THEN 
3730     PRINT "Error: Arbol vacio"
3740     STOP
3750   END IF 
3760   :
3770   lista$=""
3780   inorden A_RAIZ,hastanivel,lista$,1
3790   RETurn lista$
3800 END DEFine 
3810 :
3820 DEFine PROCedure inorden(nodo,nivel,cadena$,actual)
3830   IF nodo <> P_NULO THEN 
3840     inorden ArrIzq(nodo),nivel,cadena$,actual+1
3850     IF (nivel < 1) OR (nivel = actual) THEN 
3860       IF LEN(cadena$) <> 0 THEN cadena$ = cadena$ & ","
3870       cadena$ = cadena$ & "[" & actual & "]" & ArrArbol(nodo)
3880     END IF 
3890     inorden ArrDer(nodo),nivel,cadena$,actual+1
3900   END IF 
3910 END DEFine 
3920 :
3930 REMark --------------------------------------------------------------
3940 REMark -- ARBOL_DELETE(valor) -- Borra un elemento del arbol       --
3950 REMark --------------------------------------------------------------
3960 DEFine PROCedure ARBOL_DELETE(valor)
3970   LOCal nodo,padre,repetir
3980   :
3990   nodo = A_RAIZ
4000   padre = P_NULO
4010   REPeat repetir
4020     IF nodo = P_NULO THEN EXIT repetir
4030     IF ArrArbol(nodo) = valor THEN EXIT repetir
4040     padre = nodo
4050     IF ArrArbol(nodo) < valor THEN nodo = ArrDer(nodo)
4060     IF ArrArbol(nodo) > valor THEN nodo = ArrDer(nodo)
4070   END REPeat repetir
4080   :
4090   IF (nodo = P_NULO) THEN 
4100     PRINT "ERROR: Intenta eliminar un elemento que no existe"
4110     STOP
4120   END IF 
4130   :
4140   ARBOL_BORRAR_NODO nodo,padre : REMark Borrar el nodo
4150   A_NRO = A_NRO - 1            : REMark reducir elementos del arbol
4160   A_NIVELES = ARBOL_MAX_NIVEL  : REMark Recalcula niveles
4170 END DEFine 
4180 :
4190 REMark --------------------------------------------------------------
4200 :
4210 DEFine PROCedure ARBOL_BORRAR_NODO(nodo,padre)
4220   LOCal nuevo
4230   :
4240   :: ArrArbol(nodo) = 0
4250   :
4260   REMark Este elemento sera el que reemplace en el padre
4270   nuevo = P_NULO
4280   :
4290   REMark Si no tiene hijos, se desapunta el padre
4300   IF (ArrIzq(nodo)=P_NULO)  AND (ArrDer(nodo)=P_NULO) THEN 
4310     nuevo = P_NULO
4320   END IF 
4330   :
4340   REMark Solo tiene un hijo, se pasa al padre
4350   IF (ArrIzq(nodo)=P_NULO)  AND (ArrDer(nodo)<>P_NULO) THEN 
4360     nuevo = ArrDer(nodo)
4370   END IF 
4380   IF (ArrIzq(nodo)<>P_NULO) AND (ArrDer(nodo)=P_NULO)  THEN 
4390     nuevo =  ArrIzq(nodo)
4400   END IF 
4410   :
4420   REMark Tiene dos hijos, pasa al padre el derecho y lo borro
4430   IF (ArrIzq(nodo)<>P_NULO) AND (ArrDer(nodo)<>P_NULO) THEN 
4440     nuevo = ArrDer(nodo)
4450     ARBOL_BORRAR_NODO ArrDer(nodo),nodo
4460   END IF 
4470   :
4480   REMark Ahora apunto el padre de forma adecuada
4490   IF padre = P_NULO THEN 
4500     A_RAIZ = nuevo
4510   ELSE 
4520     IF ArrIzq(padre) = nodo THEN ArrIzq(padre) = nuevo
4530     IF ArrDer(padre) = nodo THEN ArrDer(padre) = nuevo
4540   END IF 
4550   ArrIzq(nodo) = P_VACIO
4560   ArrDer(nodo) = P_VACIO
4570 END DEFine 
4580 :
4590 REMark --------------------------------------------------------------
4600 REMark -- ARBOL_CAMBIA -- Cambia el tama‰o del arbol             --
4610 REMark --------------------------------------------------------------
4620 DEFine PROCedure ARBOL_CAMBIA(tam)
4630   LOCal tEle(A_TAM),tIzq(A_TAM),tDer(A_TAM),tTam,tRaiz,tNro,m1,m2,i
4640   :
4650   REMark Guardo el arbol en el auxiliar de forma compacta
4660   tRaiz = A_RAIZ
4670   tNro  = A_NRO
4680   tTam  = A_TAM
4690   m1    = 0      : REMark Ultimo elemento libre de la lista
4700   FOR i=A_TAM TO 0 STEP -1
4710     IF (m1 = 0) AND (tEle(i) <> P_VACIO) THEN 
4720       m1 = i + 1
4730     END IF 
4740     tEle(i)=ArrArbol(i)
4750     tIzq(i)=ArrIzq(i)
4760     tDer(i)=ArrDer(i)
4770   END FOR i
4780   :
4790   REMark calculo tama‰os m“nimos
4800   m2 = A_NRO + A_FACTOR
4810   IF tam < m2       THEN tam = m2
4820   IF tam < A_FACTOR THEN tam = tam + A_FACTOR
4830   IF tam < m1       THEN tam = m1 + A_FACTOR
4840   :
4850   REMark Creo el nuevo arbol y lo relleno
4860   ARBOL_NUEVO tam,A_FACTOR
4870   :
4880   A_RAIZ = tRaiz
4890   A_NRO  = tNro
4900   FOR i=0 TO m1-1
4910     ArrArbol(i) = tEle(i)
4920     ArrIzq(i)   = tIzq(i)
4930     ArrDer(i)   = tDer(i)
4940   END FOR i
4950 END DEFine 
4960 :
4970 :
4980 REMark --------------------------------------------------------------
4990 REMark -- n = ARBOL_NIVELES -- Retorna el nro de niveles del arbol --
5000 REMark -- RETORNA: Nivel maximo del arbol                          --
5010 REMark --------------------------------------------------------------
5020 DEFine FuNction ARBOL_MAX_NIVEL
5030   RETurn nivel_inorden(A_RAIZ, 0)
5040 END DEFine 
5050 :
5060 DEFine FuNction nivel_inorden(nodo,actual)
5070   LOCal n1,n2
5080   :
5090   IF nodo = P_NULO THEN 
5100     RETurn actual
5110   ELSE 
5120     n1 = nivel_inorden(ArrIzq(nodo),actual+1)
5130     n2 = nivel_inorden(ArrDer(nodo),actual+1)
5140     IF (n1 > n2) THEN RETurn n1 : ELSE RETurn n2
5150   END IF 
5160 END DEFine
 


Para poder verificar que el árbol funciona uso un pequeño programa de pruebas, en el se ve siempre el árbol y sus enlaces izquierdo y derecho, los punteros nulos, los elementos libres, etc. Tras cada prueba se presenta el árbol y la lista de elementos ordenados que se obtiene del mismo.

100 REMark ----------------------------------------
110 REMark -- Carga                              --
120 REMark ----------------------------------------
130 MODE 4: PAPER 0 : CLS
140 INPUT "Cual prefiere (1)=Simple",t$
150 IF t$<>"1" THEN GO TO 200
160 n$="mdv1_ed_arbol_arreglo" & t$
170 PRINT "Cargando ";n$
180 MERGE n$
190 REMark ----------------------------------------
200 REMark -- Rutinas de pruebas                 --
210 REMark ----------------------------------------
220 MODE 4: PAPER 0 : CLS : LET No=0 : LET Si=1
230 p2$=""
240 t1=DATE
250 :
260 PRINT "Creacion del arbol"
270 crear : ver : PRINT
280 :
290 PRINT "PRUEBA 1: Introduce elementos en orden, inversos y aleatorios"
300 crear : FOR i=1 TO 5         : mete(i)          : END FOR i : verarbol
310 crear : FOR i=5 TO 1 STEP -1 : mete(i)          : END FOR i : verarbol
320 crear : FOR i=1 TO 10        : mete(RND(1 TO 9)): END FOR i : verarbol
330 :
340 PRINT "PRUEBA 2: Introduce 9, elimina 5, introduce 3, elimina 4"
350 crear
360 FOR i=1 TO 9   : mete(i)
370 FOR i=1 TO 5   : borra(i)
380 FOR i=10 TO 12 : mete(i)
390 FOR i=9 TO 11   : borra(i)
400 verarbol
410 :
420 PRINT "PRUEBA 3: Introduce 9, Extrae 2, introduce 10 y reduce"
430 crear
440 FOR i=1 TO 9   : mete(i)
450 FOR i=1 TO 2   : borra(i)
460 FOR i=10 TO 19 : mete(i)
470 verarbol
480 PRINT "CM "; : ARBOL_CAMBIA 0 : ver : verarbol
490 :
500 PRINT "PRUEBA 4: Buscar algunos elementos"
510 crear
520 FOR i=1 TO 9   : mete(i)
530 FOR i= 0 TO 13 STEP 3
540   PRINT "BUSCO ";i;" = ";ARBOL_BUSCAR(i)
550 END FOR i
560 PRINT
570 :
580 PRINT "PRUEBA 5: Extrae elementos hasta error por vacia"
590 FOR i=1 TO ARBOL_LEN : borra(i)
600 t2 = DATE : PRINT "Tarda ";t2-t1;" segundos"
610 borra(0)
620 STOP
630 :
640 :
650 DEFine PROCedure crear
660   ARBOL_CREAR
670 END DEFine 
680 :
690 DEFine PROCedure mete(a)
700   PRINT "EN ";a;":";
710   ARBOL_ADD(a)
720   ver
730 END DEFine 
740 :
750 DEFine PROCedure borra(a)
760   PRINT "BO ";!a;"(";ARBOL_BUSCAR(a);"):";
770   ARBOL_DELETE(a) : ver
780 END DEFine 
790 :
800 DEFine PROCedure verarbol
810   PRINT "ARBOL: ";ARBOL_VER$(0) : PRINT
820 END DEFine 
830 :
840 DEFine PROCedure ver
850   LOCal xx
860   :
870   PRINT "[";ARBOL_LEN;"x";ARBOL_NIVELES;"]";
880   IF A_RAIZ=P_NULO THEN PRINT "*"; : ELSE PRINT A_RAIZ;":";
890   FOR xx=0 TO A_TAM
900     PRINT !"(";xx;")";ArrArbol(xx);
910     IF ArrIzq(xx)=-1 THEN PRINT "*";
920     IF ArrIzq(xx)=-2 THEN PRINT "v";
930     IF ArrIzq(xx)>0  THEN PRINT ArrIzq(xx);
940     IF ArrDer(xx)=-1 THEN PRINT "*";
950     IF ArrDer(xx)=-2 THEN PRINT "v";
960     IF ArrDer(xx)>0  THEN PRINT ArrDer(xx);
970   END FOR xx
980   PRINT
990 END DEFine 

Cuando ejecutéis las pruebas os daréis cuenta de que si introduzco los elementos de forma ordenada todos van a parar siempre a la rama derecha, quedando la izquierda vacía, por lo que tenemos una lista en lugar de un árbol. Cuando los introduzco de forma aleatoria se van llenando las ramas izquierda y derecha mas o menos a la par. Esto nos indica que un árbol binario de búsqueda solo será eficiente si los elemento se introducen al azar y producen un árbol en el que las ramas sean todas mas o menos del mismo tamaño, pues sin ese equilibrio pierde mucha efectividad en altas y bajas, pero lo que es peor, hace mas lentas las búsquedas, siendo nuestro objetivo que estas sean lo mas eficientes posibles. En la siguiente entrada montaremos arboles binarios de búsqueda equilibrados mediante las técnicas de rotación simple y doble, produciendo árboles AVR.

Como siempre aquí todo lo desarrollado sobre estructuras de datos hasta el momento, la parte de ordenación no ha variado.

Monday, 08. July 2024

NI CVI/LabVIEW and monitors with different scaling

11:51 GMT (over a year ago)

There is a known bug in NI’s CVI (and apparently LabVIEW) products that makes the user interface misbehave when the PC is connected to multiple monitors that have different scale settings: https://knowledge.ni.com/KnowledgeArticleDetails?id=kA00Z0000004AZ9SAM

The “solution” mentioned in the article is to have the same scaling factor for all monitors or disable the scaling of the specific applications. That might have been (almost) reasonable in 2016, when this issue first came up and different scaling was probably just a misconfiguration, but in times where an external monitor might be high-DPI (and thus needs a high zoom factor) and the other one is e.g. a normal laptop display, this is not possible. Furthermore, if it only affected the development computers, ok, but to tell all my users(!) to “change your Windows settings for my app to work correctly” is downright embarrassing and reflects badly on me as a developer.

Thus, I filed a bug report with NI, and after apparently much deliberation it was returned as WONTFIX, as there are the workarounds mentioned above. Let me repeat: embarrassing.

So I once again got out my trusty disassembler and investigated this issue myself. Spoiler alert: it’s easy to fix, even without patching the runtime code.

The basic problem is that the CVI runtime employs a window, hidden away at coordinate 25000×25000, for some of its tasks. This has often be a source of trouble because its creation happens before my main() function is invoked, which can have many undesired side-effects I won’t go into at this point. Anyway, this hidden window seems to adapt the scaling factor of the “nearest” monitor and as long as the app is on that monitor, everything is fine, but if it’s on a different one, hilarity ensues.

My home-office monitor configuration with one monitor deliberately set to 125% zoom

The problem now is that, when a menu is created, the hidden window is given as its parent instead of the window the menu actually belongs to 🤷‍♂️! The menu then adopts the scaling of the hidden window, no matter which monitor it is actually on, and things start to break. This should be trivial to fix in the runtime, but as I don’t feel like binary patching and distributing a hacked runtime, maybe we can do better.

I found out that, when I hide the window (by actually marking it “not visible”, not just by moving it out of the way), Windows doesn’t adopt the scale of the hidden window but takes the scale of the monitor the new window actually appears on! Bingo, that’s what we want! The only caveat: CVI uses the hidden window for the taskbar button, so we lose that. Uh-oh, not good! But fear not, there is an easy way to make the actual (visible) main window get a button instead, so the solution in the end is fairly easy with (so far) no negative side effects:

#include <windows.h>
[...]

// Workaround for multi-monitor DPI scaling issues with CVI (mainly menus not appearing where they should be):
// CVI apps have a hidden main window at position 25000x25000, so always out of view and it uses this window
// as the parent for any menu that is shown. Unfortunately, the hidden window adopts the DPI scaling of the 
// nearest monitor and inherits it to the new window. That's fine if the new window is on that monitor, but if
// it is shown on a monitor with different DPI scaling then nothing fits anymore, it's drawn too big/small and
// at the wrong location.
//
// By just removing the WS_VISIBLE style of the (anyway hidden) window Windows changes its behaviour
// completely and the new window adopts the DPI scaling of the monitor where it is actually positioned.
// 
// Caveat: the hidden window is used for the taskbar button. If we hide that, we get the proper DPI behaviour, 
// but no taskbar button! So, as a further workaround, we enable the ES_EX_APPWINDOW style on the ACTUAL main
// panel below, which will then get its own taskbar button, with an even better working preview!
SetSystemAttribute(ATTR_TASKBAR_BUTTON_VISIBLE, 0);

[...]

if ((panelHandle = LoadPanel(0, "demo.uir", PANEL)) < 0)
	return -1;
	
// Second part of the DPI fix above, let's give our main window a taskbar button after all
HWND hwnd;
GetPanelAttribute(panelHandle, ATTR_SYSTEM_WINDOW_HANDLE, (intptr_t*)&hwnd);
SetWindowLongPtr(hwnd, GWL_EXSTYLE, GetWindowLongPtr(hwnd, GWL_EXSTYLE) | WS_EX_APPWINDOW);

As you can see, the workaround consists of only 3 lines of code. After including those, everything works as expected, menus draw consistently at the size and position they are supposed to show up, no matter the monitor configuration or monitor they are on.

All in all it took me less than a day to figure all this out, it was almost less work than filing and seeing through the bug report mentioned above (my support contact was very dedicated and determined, but in the end as helpless as me when R&D says they don’t want to fix it). NI on the other hand has listed this as a known issue for 8 years, and they have the source code! But I’ve heard these are very tumultuous times for them after the acquisition, I wish everybody the best and hope that someday they can correct course and maybe return to their old form. Long live CVI 😉

Sunday, 23. June 2024

Retrowiki

09:09 GMT (over a year ago)
Screen dump of the Retrowiki site.

This is a Spanish forum for Sinclair computers, anything from ZX80 to QL and the Sinclair PC, as well as Spectrum clone machines.

It’s a forum type of website – if you are on QL Forum, for example, you’ll be familiar with navigating this kind of website.

Screen dump of Right-click menu for translating website

It’s written in the Spanish language. Anyone who doesn’t speak Spanish can use the translation features in modern browsers to translate to another language. For example, in the Edge browser I just right click somewhere in the forum window, then select “Translate to English”.

The Retrowiki is organised by computer system – separate sections for QL, Spectrum, ZX80/ZX81 and the Sinclair PC.

Nice to see that the QL has the second highest number of topics and messages, just after Spectrum software.

When I browsed the site this morning, I noticed there were some posts from users outside Spain too, for example from Derek Stewart (who builds and sells Q68 computers in the U.K.)

Great to see such a lot of Sinclair computer activity still happening in Spain.

To visit the Retrowiki, just point your browser at https://retrowiki.es