domingo, 17 de mayo de 2026

La Europa olvidada de los 8 bits

Los microordenadores europeos de 8 bits que no llegaron a España

Durante los años 80, Europa era un mosaico de ordenadores incompatibles entre sí. Más allá de los sistemas que triunfaron en España, existía todo un universo paralelo de máquinas fascinantes que aquí apenas llegaron a existir.

Cuando pensamos en la informática doméstica de los años 80, solemos recordar unas pocas máquinas icónicas: Spectrum, Commodore, Amstrad o MSX fueron los líderes. Sin embargo, mientras en España el mercado estaba muy concentrado en estas máquinas, en el resto de Europa existía una diversidad de ordenadores de 8 bits, muchos de ellos desconocidos aquí.

Este artículo es un pequeño viaje por esa Europa informática olvidada: máquinas interesantes, a veces brillantes, que no tuvieron presencia real en nuestro país.


🇸🇪 Luxor ABC80 (1978): el pionero escandinavo

El Luxor ABC80 (1978) fue uno de los primeros microordenadores europeos de gran difusión en su mercado local, especialmente en Suecia. Basado en un procesador Z80, se utilizó ampliamente en educación y pequeñas empresas. Su éxito en Escandinavia contrasta con su casi total desconocimiento en el sur de Europa.

  • Procesador: Zilog Z80A (a 3.5 MHz)
  • RAM: 16 KB (ampliable a 32 KB)
  • ROM: 16 KB (con BASIC integrado)
  • Gráficos: Modo texto (40x24 caracteres) y gráficos de bloques monocromo (78x72 píxeles)
Luxor ABC80 Imagen: Wikimedia Commons (CC BY-SA)

🇬🇧 Grundy NewBrain (1981): el micro con pantalla propia

El Grundy NewBrain (1981) es uno de los ordenadores más peculiares de la época. Diseñado originalmente como un proyecto de Sinclair y posteriormente desarrollado por Grundy Business Systems, destacaba por incluir una pequeña pantalla en la propia carcasa, conexión a TV o Monitor.

Con gráficos de alta resolución monocromos, basado en un procesador Z80 y con un BASIC bastante potente que disponía de muy buen manejo de números en coma flotante. Estaba orientado a un uso más serio que lúdico, por lo que se usó mucho para temas científicos.

Incluía una pantalla de una sola línea, lo que permitía interactuar con el sistema sin necesidad de monitor externo, algo casi único en los microordenadores domésticos de principios de los 80 (hay otro equipo canadiense de 1974 que usaba una pantalla similar, el MCM/70). Las bajas ventas del aparato hizo que sacaran un modelo sin el display para abaratar el coste.

  • Procesador: Zilog Z80A (a 4 MHz)
  • RAM: 32 KB (ampliable hasta 2 MB)
  • ROM: 28 KB (incluye NewBrain BASIC y software de oficina)
  • Gráficos: Pantalla VF integrada (1 línea de 16 caracteres). En monitor externo: hasta 640x256 píxeles (monocromo) o 320x256 píxeles (4 colores)
Grundy NewBrain Imagen: Wikimedia Commons (CC BY-SA)

🇬🇧 Jupiter Ace (1982): el rebelde que hablaba Forth

Creado por Richard Altwasser y Steven Vickers (diseñadores clave del ZX Spectrum), el Jupiter Ace es una de las grandes rarezas de la informática británica.

A diferencia de casi todos sus rivales, que usaban el lenguaje BASIC, este microordenador apostaba por el lenguaje Forth en su memoria ROM. Esto lo hacía increíblemente rápido y eficiente con solo 3 KB de RAM.

Su aspecto modesto con teclas de goma blanca y su falta de capacidades gráficas avanzadas provocaron que vendiera muy pocas unidades, convirtiéndolo hoy en una cotizadísima pieza de coleccionista que apenas se vio en España. Para el mercado americano fabricaron la versión Jupiter Ace 4000, con una caja igual pero con plástico de mejor calidad y salida para monitor.

  • Procesador: Zilog Z80A (a 3.25 MHz)
  • RAM: 3 KB (ampliable externamente a 16 KB o 48 KB)
  • ROM: 8 KB (con el entorno y compilador interactivo Forth integrado)
  • Gráficos: Modo texto estricto (32x24 caracteres). Gráficos redefinibles mediante bloques monocromáticos de baja resolución (64x48 píxeles)
Jupiter Ace  
Imagen: Wikimedia Commons (CC BY-SA)

🇬🇧 Acorn Electron (1983): la evolución del estándar británico

Acorn fue uno de los actores clave en Reino Unido. El BBC Micro (1981) se convirtió en un estándar educativo, muy usado a partir del programa de la Televisión BBC sobre ordenadores, donde se enseñaba a manejar el aparato, y se incluyó en los colegios para enseñar informática. Evolucionó en el Acorn Electron (1983), que intentó llevar esa tecnología a un mercado doméstico más asequible.

El BBC Micro destacaba por su potencia y expansión, mientras que el Electron ofrecía una versión simplificada y más económica. Aunque tuvieron un enorme impacto en Reino Unido, en España su presencia fue muy limitada, y el Electron es muy poco conocido.

  • Procesador: MOS Technology 6502 (a 2 MHz en BBC Micro / 1 MHz en Electron)
  • RAM: 16 KB o 32 KB (BBC Micro) / 32 KB (Electron)
  • ROM: 32 KB (BBC) / 32 KB (Electron)
  • Gráficos: Múltiples modos; máximo de 640x256 píxeles (2 colores) o 160x256 píxeles (16 colores en BBC / 4 colores en Electron)
Acorn BBC Micro 
 Imagen: Wikimedia Commons (CC BY-SA)

🇫🇷 Matra Alice (1983): minimalismo francés

El Matra Alice (1983), fue un ordenador doméstico francés basado en el Motorola 6803. Orientado a la iniciación, apostaba por un diseño compacto, colores llamativos y un enfoque educativo. Su simplicidad y limitaciones técnicas hicieron que quedara relegado frente a sistemas más potentes.

  • Procesador: Motorola 6803 (a 0.89 MHz)
  • RAM: 4 KB (el Alice 32 posterior subió a 16 KB)
  • ROM: 8 KB (con Microsoft BASIC)
  • Gráficos: Gestionado por el chip Motorola 6847. Modo texto y gráficos semigráficos con un máximo de 9 colores disponibles en baja resolución
Matra Alice  
Imagen: Wikimedia Commons (CC BY-SA)

🇬🇧 Memotech MTX (1983): ingeniería brillante

El Memotech MTX (1983) fue un ordenador británico basado en Z80 con características avanzadas, dentro de una caja de forma similar a la del Commodore 64. Permitía ampliaciones de memoria poco habituales en su época y utilizaba un chip gráfico similar al estándar MSX. A pesar de su calidad, llegó a un mercado saturado y no alcanzó el éxito comercial que seguramente merecía, siendo uno de los grandes olvidados.

  • Procesador: Zilog Z80A (a 4 MHz)
  • RAM: 32 KB (MTX500) o 64 KB (MTX512), ampliable a 512 KB
  • ROM: 24 KB (con MTX BASIC y ensamblador/desensamblador de código máquina)
  • Gráficos: Chip Texas Instruments TMS9918. Resolución de 256x192 píxeles con una paleta de 16 colores y soporte para 32 sprites por hardware
Memotech MTX  
Imagen: Wikimedia Commons (CC BY-SA)

🇬🇧 Oric-1 (1983) y Oric Atmos (1984): buenas ideas, mala suerte

El Oric-1 (1983) fue un ordenador doméstico basado en un procesador 6502, con 16 KB de RAM, teclado de membrana y un BASIC competente.

El Oric Atmos (1984) mejoró el diseño con un buen teclado mecánico y 64 KB de RAM, pero los problemas empresariales y la falta de software limitaron su éxito.

  • Procesador: MOS Technology 6502A (a 1 MHz)
  • RAM: 16 KB o 48 KB (Oric-1) / 48 KB (Oric Atmos)
  • ROM: 16 KB (con Oric Extended BASIC)
  • Gráficos: Modo alta resolución de 240x200 píxeles con 8 colores disponibles (limitaciones de atributos de color por filas, similar al Spectrum)
Oric 1 
 Imagen: Wikimedia Commons (CC BY-SA)
Oric Atmos 
 Imagen: Wikimedia Commons (CC BY-SA)

🇫🇷 Thomson MO5 (1984): la informática impulsada por el Estado

En Francia, el gobierno apostó por introducir la informática en las escuelas, y uno de los ordenadores protagonistas fue el Thomson MO5 (1984).

Equipado con un procesador Motorola 6809 y gráficos de hasta 320x200 píxeles, fue utilizado ampliamente en programas educativos nacionales.

Sin embargo, su fuerte enfoque educativo y su ecosistema cerrado limitaron su expansión fuera del país. La carcasa era de color negro, pero existieron algunas variantes, como una versión especial firmada por Platini, un famoso jugador de fútbol francés de la época.

  • Procesador: Motorola 6809E (a 1 MHz)
  • RAM: 48 KB (ampliable a 64 KB)
  • ROM: 16 KB (con Microsoft BASIC)
  • Gráficos: Resolución de 320x200 píxeles, capaz de mostrar 16 colores simultáneos (con restricciones de proximidad de color)
Thomson MO5  
Imagen: Wikimedia Commons (CC BY-SA)

🇳🇱 Philips VG-5000 (1984): el intento previo al MSX

Antes de apostar por el estándar MSX, Philips lanzó en Europa el VG-5000 (1984), un ordenador de 8 bits bastante modesto.

Su arquitectura limitada y su escaso catálogo de software hicieron que quedara rápidamente eclipsado, pero es un buen ejemplo de la experimentación previa a la aparición de estándares comunes.

  • Procesador: Zilog Z80A (a 4 MHz)
  • RAM: 24 KB (ampliable a 56 KB)
  • ROM: 16 KB (con un dialecto propio de Microsoft BASIC)
  • Gráficos: Gestionado por el procesador de vídeo SGS Thomson EF9345. Modo semigráfico con una resolución equivalente de 320x250 píxeles y paleta de 8 colores
Philips VG-5000 
Imagen: system-cfg.com

🇭🇺 Videoton TVC (1984): el orgullo del Bloque del Este

Fabricado en Hungría por la empresa estatal Videoton (conocida por sus televisores), el TV Computer (TVC) se basaba en el procesador Z80 y estaba enfocado al sistema educativo de su país.

Contaba con un joystick integrado en la carcasa, un teclado bastante robusto y un BASIC potente capaz de manejar gráficos a color muy avanzados para los estándares de Europa Oriental. Fuera de las fronteras húngaras y de ciertos acuerdos de exportación muy limitados dentro del Bloque del Este, este robusto microordenador fue un auténtico fantasma para el mercado occidental.

  • Procesador: Zilog Z80A (a 3.125 MHz)
  • RAM: 32 KB o 64 KB (según la versión instalada de fábrica)
  • ROM: 20 KB (con el sistema operativo propio y el intérprete de TVC-BASIC)
  • Gráficos: Tres modos diferentes; máximo de alta resolución en 512x240 píxeles (2 colores), modo intermedio en 256x240 píxeles (4 colores) o modo color en 128x240 píxeles (16 colores)
Videoton TVC 
 Imagen: Wikimedia Commons (CC BY-SA)

🇭🇺🇬🇧 Enterprise 64/128 (1985): adelantado a su tiempo

El Enterprise 64/128 fue lanzado en 1985 tras varios retrasos. Diseñado en Reino Unido y producido en un contexto empresarial complicado, muchas unidades terminaron en Hungría.

Destacaba por sus chips personalizados para gráficos ("Nick") y sonido ("Dave"), así como por un sistema de memoria muy avanzado.

Pero llegó tarde y con poco software disponible, lo que impidió su éxito comercial.

  • Procesador: Zilog Z80A (a 4 MHz)
  • RAM: 64 KB o 128 KB (arquitectura ampliable internamente hasta un máximo teórico de 4 MB)
  • ROM: 32 KB (EXOS, sistema operativo propio) + 16 KB adicionales en cartucho de lenguaje IS-BASIC
  • Gráficos: Chip a medida "Nick". Múltiples resoluciones desde baja densidad hasta un máximo de 640x512 píxeles entrelazado. Paleta total de 256 colores (hasta 256 colores simultáneos en pantalla según el modo)
Enterprise 128 
 Imagen: Wikimedia Commons (CC BY-SA)

🇩🇪 Robotron KC85 (1985–88): informática tras el Telón de Acero

En la Alemania del Este, la informática doméstica seguía un camino propio. El Robotron KC85 es uno de los ejemplos más representativos.

Basado en un clon del Z80, se utilizaba principalmente en entornos educativos y técnicos, con disponibilidad muy limitada.

Su diseño modular lo hace especialmente interesante desde el punto de vista técnico.

  • Procesador: U880D (un clon exacto de la RDA del Zilog Z80, corriendo a 1.75 MHz)
  • RAM: 16 KB o 64 KB (según el modelo KC85/2, /3 o /4), muy ampliable mediante módulos hardware físicos enchufables
  • ROM: 4 KB a 20 KB (según la revisión del sistema operativo interno y la versión del intérprete de BASIC)
  • Gráficos: Resolución fija en pantalla de 320x256 píxeles con soporte para mostrar una paleta de 16 colores
Robotron KC85  
Imagen: Wikimedia Commons (uso libre con atribución)

Conclusión

La informática de 8 bits en Europa fue mucho más diversa de lo que solemos recordar. Cada país experimentó con sus propias soluciones, dando lugar a una enorme variedad de máquinas olvidadas o desconocidas en España.

Hoy, estos ordenadores no solo son piezas de colección, sino también una ventana a una época en la que la informática aún no estaba estandarizada.

Explorarlos es, en cierto modo, redescubrir una historia alternativa de la informática.

¿Conocías alguno de estos sistemas? Si recuerdas algún otro micro europeo poco conocido, déjalo en los comentarios. La historia de los 8 bits aún tiene muchos rincones por descubrir.

viernes, 1 de mayo de 2026

ZX2SB: Cambio de idea de los temas especiales

Í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.

martes, 28 de abril de 2026

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

Í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).

lunes, 20 de abril de 2026

ZX2SB: Otra vuelta atrás

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)

viernes, 17 de abril de 2026

ZX2SB: El generador de código SuperBASIC

Í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…

jueves, 16 de abril de 2026

ZX2SB: El Analizador semántico

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.

lunes, 13 de abril de 2026

ZX2SB: El Parser o Analizador Sintáctico

Í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.