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

No hay comentarios:

Publicar un comentario