viernes, 13 de marzo de 2026

ZB2SB. Paso 2. Director del proyecto

Transpilador ZX BASIC — El Director del Proceso (Orquestador del Pipeline)

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

jueves, 12 de marzo de 2026

ZB2SB. Paso 1. Definir el Lenguaje de origen

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

miércoles, 11 de marzo de 2026

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

Í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

martes, 3 de marzo de 2026

Esperar y Soñar: La Magia Olvidada de los Juegos en Cassette

Esperar y Soñar: La Magia Olvidada de los Juegos en Cassette


Hubo un tiempo en el que jugar no empezaba con un botón que decía Play, sino con un ritual. Un rito lento, sonoro y casi hipnótico: colocar el cassette, ajustar el volumen y cruzar los dedos para que la carga llegara al 100% sin errores. En la era de la inmediatez, ese proceso parece casi absurdo, pero para muchos fue la puerta de entrada a mundos increíbles, aunque estuvieran hechos de ocho colores y píxeles del tamaño de un puño.

En esta entrada quiero rendir homenaje a una de las experiencias más icónicas —y a menudo olvidadas— de los queridos ordenadores de 8 bits: la carga de juegos en cassette.


El sonido que encendía la imaginación

Dicen que la nostalgia también tiene sonido. Para muchos, es una serie de pitidos chirriantes que se repetían durante varios minutos. Aquellos ruidos que hoy nos suenan caóticos eran, en realidad, una melodía digital única. Cada máquina tenía su propio patrón, y dentro de ellas, cada juego su propio “canto”. Había quien presumía en el patio del colegio de distinguir un Spectrum de un MSX solo por el sonido de carga (un talento inútil, pero que en aquella época sonaba a magia).

Lo que se grababa en la cinta eran pitidos: una frecuencia representaba el cero y otra el uno. Debían ser audibles porque las cintas domésticas no eran capaces de registrar señales digitales directas. El concepto venía heredado de los módems y de las comunicaciones por línea telefónica: transformar bits en tonos que cualquier magnetófono pudiera grabar. Sencillo, ingenioso y tremendamente limitado, pero funcionaba. 

Pantalla de carga en un Spectrum (Fuente: retrogaming)


El arte de cargar un juego

Cargar un juego no era trivial. Había que seguir una serie de pasos casi ceremoniales, y cada sistema tenía sus manías y peculiaridades.

  • Conectar el cassette. Cada ordenador usaba un sistema de lectura distinto. Podemos agruparlos en dos grandes tipos:
    • Unidades analógicas: eran las normales de casa. La CPU del ordenador se encargaba de procesar los tonos grabados en la cinta, a veces con ayuda de chips como la ULA en el Spectrum. Esto permitía modificar las rutinas de carga para acelerar el proceso (cargas TURBO) o añadir sistemas de protección anticopia. Pero tenía una limitación: mientras cargaba, el ordenador estaba “secuestrado”.
      • ZX Spectrum y MSX: usaban grabadoras domésticas o walkman. Se conectaban por cables de audio para lectura y grabación, y en el caso de MSX un tercer cable activaba el motor automáticamente. Era habitual pelearse por la grabadora familiar, y nacieron unidades como el Computone, que prometían “cargar siempre a la primera”. No era del todo cierto, pero ayudaba.
      • Amstrad CPC 464: incluía una unidad integrada igual a una doméstica, pero más estable. No hacía el sonido estridente del Spectrum y controlaba el motor directamente, lo que evitaba muchos errores.
      • Amstrad CPC 6128: incluía disquetera interna, pero mantenía conectores para usar una grabadora doméstica si era necesario.
    • Unidades digitales: en ellas, la grabadora incorporaba un circuito que convertía los tonos de la cinta en ceros y unos antes de enviarlos al ordenador. Esto liberaba a la CPU y permitía pequeñas maravillas, como minijuegos que se ejecutaban mientras el juego principal cargaba.
      • Commodore y Atari: usaban unidades dedicadas, no servían para música, pero eran fiables y difíciles de “desajustar”. Aunque podían fallar, eran mucho más estables que las analógicas.
  • El volumen perfecto. En las cargas analógicas era vital. Muy alto, saturaba; muy bajo, no distinguía las frecuencias. La delgada línea entre jugar o encontrarte con el maldito Tape loading error.
  • La paciencia. Los tiempos de carga solían ser de 3 a 10 minutos, pero algunos juegos superaban los 20.
  • La esperanza. Si fallaba al 97% tocaba empezar de cero. ¿Frustrante? Sí. ¿Épico? También. Y aun así, lo repetíamos una y otra vez. 

Un Walkman de Sony (Fuente: amazonaws.com)

 

El muy deseado Computone (fuente: teknoplof.com)

Casetera del Commodore-64 (Fuente: pinimg.com)


Cuando la carga era parte del juego

Algunos desarrolladores convirtieron la pantalla de carga en un espectáculo. Una oportunidad para entretener al jugador mientras los datos fluían desde la cinta:

  • Pantallas multicolor en el Spectrum, auténticas obras de arte que aprovechaban las limitaciones para crear efectos vibrantes.
  • Mini-demos o barras animadas que mostraban el progreso real del cargado.
  • Cargas turbo, cada vez más veloces y experimentales.
  • Mini-juegos, especialmente en Commodore 64 o Atari ST, donde podías jugar mientras el juego “gordo” seguía cargando.

Algunos títulos mostraban arte, mapas o información del juego durante la carga: las primeras loading screens de la historia, con personalidad propia.

La piratería "artesanal"

Las cintas se podían copiar fácilmente. ¿Quién no tuvo un amigo que pasaba horas duplicando juegos con una doble pletina? O aquellas recopilaciones caseras con títulos escritos a mano, a veces con esmero, a veces con letra imposible.

Era otro mundo: más inocente, más casero y más emocionante. Y aquel caos doméstico impulsó a muchos desarrolladores a idear elaborados sistemas anticopia (tema que trataste en tu entrada anterior). 

Unidad de doble pletina, un lujo para las copias. (Fuente: dreamstime.com)


¿Por qué recordarlo hoy?

En un tiempo donde todo está a un clic, recordar la era del cassette nos devuelve una verdad sencilla: las cosas que cuestan se disfrutan más.

Cargar un juego era parte de la aventura. Un preludio que hacía que cada partida supiera a triunfo. No era perfecto, pero era auténtico. Y por eso permanece en la memoria.


¿Y tú? ¿Qué recuerdos tienes?

¿Tenías un Spectrum, un CPC, un MSX o un Commodore 64?
¿A qué juego jugaste primero en cassette?
¿Alguna carga fallida al 97% que aún recuerdes con escalofríos?

Me encantará leer tus anécdotas en los comentarios.

Enlaces recomendados

lunes, 2 de marzo de 2026

Los códigos de protección anticopia en la era de los 8 bits y 16 bits

Descubre los métodos anticopia más creativos y sorprendentes de los años 80 y 90: ruedas de códigos, Lenslok, sectores ilegales, cargadores turbo y otras técnicas míticas de la informática retro.

🛡️ Los códigos de protección anticopia más ingeniosos de la era de los 8 y 16 bits

En los años 80 y principios de los 90, cuando los ordenadores domésticos vivían su edad de oro, la batalla contra la piratería se libraba con imaginación, bordes técnicos y una buena dosis de pillería creativa. Los videojuegos se distribuían primero en cinta y, más tarde, también en disquete: soportes sencillos de duplicar. Las compañías desarrollaron algunos de los sistemas anticopia más extravagantes, ingeniosos y —en ocasiones— delirantemente complicados que se recuerdan en la historia del software.

Este artículo repasa algunos de los métodos más famosos —y otros quizá menos conocidos— que definieron toda una época.


🎨 1. Códigos impresos en color imposible

Para evitar que los manuales se fotocopiaran fácilmente, muchas compañías imprimían contraseñas en combinaciones de color difíciles de reproducir. Las fotocopiadoras de la época convertían ciertos colores en manchas negras o ilegibles, lo que impedía usar copias piratas. Este método también se empleó en tarjetas como las de Jet Set Willy, diseñadas para ser difíciles de duplicar.

 
Manual del Jet Set Willy con sus códigos de colores. (Fuente: computing history org)

 

📘 2. Ruedas de códigos (Code Wheels)

Compañías como Lucasfilm Games o Sierra popularizaron las icónicas ruedas de cartón multicapa que permitían alinear símbolos o imágenes para generar claves únicas. Títulos como The Secret of Monkey Island lo usaron en todas sus versiones.

 

Dial‑A‑Pirate del juego The Secret of Monkey Island –  (Fuente: OldGames.sk)
 
Monkey Island 2 – Mix’n’Mojo Code Wheel (Fuente: oldgames.sk)
The Secret of Monkey Island – Amiga (Fuente: OpenRetro.org)


 

🔬 3. Gráficos 3D y estereogramas

A finales de los 80 aparecieron protecciones visuales que requerían intervención humana directa. Algunos juegos mostraban patrones deformados legibles sólo con lentes incluidas en la caja o con gafas 3D anaglifo rojo‑azul. Sin ese accesorio, la clave era indescifrable.

Lenslok en un juego de MSX (Fuente: msx.org)

🔐 4. Tablas de códigos interminables

Se incluían libretos con enormes tablas repletas de valores en filas y columnas. El juego pedía una intersección concreta, obligándote a usar el original. Bard’s Tale III es un ejemplo bien documentado; en preservación existen tanto tablas como ruedas asociadas a distintas ediciones.

 

La rueda del Bard's Tale III, en forma de rueda pero es una tabla realmente. (Fuente: mocagh.org)

🧪 5. El infame Lenslok

El Lenslok fue uno de los sistemas anticopia más polémicos: un prisma que reorganizaba visualmente un código deformado en pantalla. Entre sus problemas documentados estaban la calibración complicada, la incompatibilidad con pantallas muy grandes o muy pequeñas y la existencia de variantes distintas por juego. [3], [4] wiki/Lenslok)

Dispositivo listo para usar (Fuente io.wp.com)

El dispositivo sin plegar con sus instrucciones (Fuente: ComputingHistory.org)

Otro dispositivo sin plegar (Fuente: SpectrumComputingHistory.org)

 

 

💽 6. Sectores y pistas “ilegales” en disquete

En Amiga, Atari ST y C64 se emplearon técnicas basadas en manipular físicamente el contenido del disco: pistas fuera de norma, sectores con longitudes no generables por hardware doméstico o marcas magnéticas alteradas. En el ecosistema Spectrum/CPC destacó Speedlock, con variantes bien documentadas.

 

Speedlock 1987 (Fuente: https://muckypaws.com) 

📼 7. Cargadores turbo con errores deliberados

En las cintas de ZX Spectrum o Amstrad CPC, los cargadores incluían anomalías intencionadas (cabeceras corruptas, pausas imposibles o bloques “dañados”) que engañaban a las copiadoras. Sistemas como Speedlock en cinta fueron clave en esta técnica.

Pantalla de carga del juego Boina Verde en un CPC (Fuente: video-games-museum.com)


🎤 8. Códigos ocultos en pistas de audio

Algunos lanzamientos poco comunes escondían códigos como sonidos digitales embebidos en la propia cinta. Parecían ruido estático, pero el programa podía interpretarlo como datos válidos. La copia directa de alta calidad era prácticamente el único modo de preservarlos.

🕶️ 9. Libretos imprescindibles para resolver puzzles

Las aventuras gráficas a menudo requerían consultar objetos físicos incluidos en la caja: notas manuscritas, mapas o pistas visuales. El “diario del Grial” de Indiana Jones and the Last Crusade es el ejemplo más célebre.

 

Manual de Indicana Jones y la última cruzada. El diario del Grial.

 



📌 Conclusión: creatividad contra creatividad

Sin conexión a internet, DRM ni activaciones online, las compañías recurrieron a la inventiva física: ruedas, lentes prismáticas, tintas imposibles, pistas alteradas y libretos clave. Hoy, estas protecciones se contemplan con nostalgia: objetos coleccionables que recuerdan una época más artesanal, humana e ingeniosa.


📚 Referencias

Si quieres profundizar, puedes leer información adicional en alguno de los siguientes enalces, en español y en inglés:
  • En español

    • 3djuegos: «Así se luchaba contra la piratería en los videojuegos en los años 80 y 90: historia Retro de una batalla eterna»
    • Xataka: «Así era el maravilloso mundo de los sistemas de protección “artesanales” para los videojuegos de antaño»
    • El confidencial : Jaume Esteve, «Camelot Warriors o el primer sistema anticopia en el ‘software’ español»
    • Jot Down : «No queremos que copies esto» (crónica sobre piratería en España y el dispositivo SD1.
    • NeoTeo: «Dial‑A‑Pirate, y otros métodos anticopia en los videojuegos viejos».
    • NeoTeo: «La protección anticopia de los videojuegos» (visión histórica general).
    • Chicas Gamers : «Sistemas anticopia curiosos de videojuegos» (repaso divulgativo).
    • AcademiaLab: «Lentelok (Lenslok)» (explicación en español del sistema con listado de juegos).

    En inglés

    • WikipediaLenslok (historia, calibración, lista de títulos).
    • CPCWikiSpeedlock (CPC): variantes, velocidades de carga, listado de juegos.
    • iFixit : «Five Weird Ways Old Games Tried to Prevent Copying» (tarjetas de colores, Jet Set Willy, Lenslok, etc.).
     

lunes, 17 de febrero de 2025

Cables de Video para Commodore VIC-20, C64, C16, C128

Ampliado el  18/02/25 en color azul
 
El Commodore VIC-20 disponía de un conector de 5 pines para la salida de video. Los primeros Commodore 16 lo usaron igualmente, pero en el C-64 solo lo tuvieron las primeras unidades, luego se amplió a un conector de 8 pines en U (cuidado que hay otro conector de 8 pines que van en círculo y no se puede usar). Este conector está presente también en el C128, que dispone de otro con señales de color. Como las señales de los primeros 5 pines eran casi las mismas, y solo usamos unas pocas en nuestros cables, y además la posición física de los pines del DIN-5 coinciden con las inferiores del DIN-8 y un conector DIN-5 se puede enchufar en un DIN-8, por lo que se puede usar un cable de 5 pines en cualquier modelo.
 
Los C64 no disponen de salidas RGB en el conector, solo de S-Video y AV, por lo que no se gana nada usando un conector SCART, pero si alguno lo quiere montar porque su tele no tenga AV, que me pase un mensaje y añadiré ese cable en la entrada.
 
Dependiendo del modelo de C64 la señal de +5V del pin 7 pueden faltar, en concreto los modelos C64 y C64A no lo llevan aunque los C64B y C64E si tienen la señal, pero las tres básicas para AV, Sonido y Masa están siempre presentes.

CABLE AV

Pongo los esquemas tanto para conector de 5 como para conector de 8 pines, pero si usáis el de 8 recordar que hay dos modelos de conector, uno con los pines en U y otro con pines en círculo, debéis usar el de pines en U.
 
En el esquema del apartado siguiente para el Euroconector he unido el pin 5 con masa, esto es opcional, según comentan mejora la calidad de salida del Audio al fijar a masa la entrada de sonido.

Cable AV a través del Euroconector

Si alguno no dispone de entradas AV pero si de Euroconector (también llamado Scart), puede usar este cable, no ganará en calidad pues es AV igualmente, solo cambia el cable que usamos. 


Cable S-VIDEO

Es el cable preferido en USA, recordar que no es Super-Video sino Separated-Video (video separado) pues lleva las señales de luna y croma por dos cables separado, mientras que el AV los lleva juntos por uno solo. La diferencia de calidad entre ambos es inapreciable, pues los televisores con S-Video suelen usar un condensador para unir Luma y Croma en su entrada, por lo que al final la señal que procesan es la misma. Cuidado que este sistema no lleva el sonido, solo el Video, por tanto hay que usar otro cable para conectarlo a una fuente externa, por lo que no tiene mucho sentido realmente ni aporta nada sobre el AV.