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.

sábado, 4 de abril de 2026

ZX2SB. Vuelta a empezar

Índice de entradas del conversor



¿De transpilador a compilador?

Evolución del proyecto y cambio de enfoque

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

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

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


El punto de partida: traducir, no reinterpretar

Desde el principio hubo varias decisiones claras:

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

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

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


El generador fue el primer aviso

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

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

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

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

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


La ruptura conceptual: dividir en módulos

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

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

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

Primer paso: el fichero .TOK

El fichero .tok es la frontera definitiva entre fases:

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

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


El papel del Director: menos es más

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

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


El resultado: un lexer cerrado

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

Esta fase está cerrada.


Conclusión

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

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


Postdata

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

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

viernes, 27 de marzo de 2026

Cuando con 64 KB eras el rey

Redescubriendo el Commodore 64: Historia, Características, Juegos y Legado Retro

Redescubriendo el Commodore 64

Mi pequeña introducción personal

Yo no tuve un Commodore 64. En casa teníamos un ZX Spectrum 48K, un gomas, pero incluso entonces el C64 era el objeto de deseo: gráficos más coloridos, un sonido impresionante, un teclado de verdad, un aire de máquina “seria”. Su precio era mayor, sí, pero también su ambición. Era el ordenador que todos mirábamos con admiración en las revistas de la época.

Un gigante en miniatura

El Commodore 64 es considerado el ordenador doméstico más vendido de un único modelo, con entre 12,5 y 17 millones de unidades vendidas. El ZX Spectrum, en todas sus variantes, vendió unos 5,5 millones. El C64 dominó en Estados Unidos, pero logró difusión mundial, mientras que el Spectrum fue esencialmente europeo.

Las raíces del C64

KIM-1 (1976)

El precursor de todo fue el KIM‑1, una placa de desarrollo popular fabricada por MOS Technology basada en su procesador MOS 6502. Tras la adquisición de MOS por parte de Commodore, esta tecnología se convirtió en el corazón de sus futuros ordenadores.

Commodore PET (1977)

El primer ordenador completo de la compañía fue el Commodore PET, lanzado en 1977. Muy presente en centros educativos, integraba monitor, carcasa y teclado. Su sucesor, el CBM-II (1982), refinó la línea con modelos profesionales y domésticos.

Commodore VIC‑20 (1981)

El VIC‑20 fue el primer ordenador personal en superar el millón de unidades vendidas. Demostró que existía un mercado masivo para la informática doméstica, aunque muchos usuarios pedían algo más potente.

Commodore 16 (1984)

Creado como alternativa económica al VIC‑20, el Commodore 16 —y su versión más barata, el Commodore 116— pasaron bastante desapercibidos.

El proyecto MOS Technology

En 1981, MOS desarrolló dos chips destinados originalmente a recreativas: el VIC‑II (vídeo) y el SID (sonido). En apenas seis semanas estos chips dieron lugar al prototipo funcional del C64.


Nacimiento del Commodore 64 (1982)

El C64 debutó en el CES de enero de 1982, causando enorme impacto por sus capacidades multimedia avanzadas y su precio competitivo, posible gracias a que Commodore fabricaba sus propios chips.


¿Por qué fue tan revolucionario?

La revolución del C64 se basó en dos chips clave:

  • VIC‑II: sprites, scroll por hardware y 16 colores, muy superior a lo habitual en 1982.
  • SID: chip de sonido analógico-digital con tres osciladores, filtros y envolventes.

Además, Commodore distribuyó el C64 en grandes superficies como Sears y K‑Mart, logrando una presencia masiva imposible para sus rivales que usaban tiendas especializadas como la cadena Radio Shark..


Características técnicas del aparato

Aunque famoso por sus 64 KB, el C64 destacaba por un equilibrio muy bien diseñado:

  • CPU: MOS 6510 a 1.02 MHz (PAL) / 0.985 MHz (NTSC).
  • RAM: 64 KB totales, no siempre accesibles simultáneamente por mapeo, dejaba libres 40Kb para el Basic. Aparte disponía de 512Kb para el manejo del color.
  • ROM: 20Kb 
  • Vídeo: VIC‑II (6567/6569), sprites, scroll, multicolor.
  • Sonido: SID 6581/8580, 3 voces, filtros, modulación.
  • Almacenamiento: Cassettes 1530, disqueteras 1541, cartuchos.
  • Puertos: 2 joysticks tipo Atari, puerto de usuario, expansión.

Spectrum/Z80 contra C64/MOS 6510

Es fácil de entender la diferencia entre ambas máquinas: no era solo de marketing, sino de las decisiones de diseño de sus creadores.

🔹 ZX Spectrum (16/48 KB)

  • El procesador Z80 trabajaba a 4 MHz, pero necesitaba varios ciclos por instrucción.
  • Usaba RAM dinámica barata, que debía refrescarse parando ligeramente al procesador; añadir más encarecía demasiado el producto.
  • La misma RAM se usaba para vídeo y programa, simplificando el diseño.
  • Mapa de memoria lineal de 64 KB: ROM + RAM directamente accesibles.
  • No incluía chips dedicados de vídeo o sonido, por lo que sus capacidades eran limitadas.

🔹 Commodore 64 (64 KB)

  • El procesador 6510 trabajaba a ~1 MHz, pero muchas instrucciones eran de un ciclo.
  • Commodore fabricaba su propia RAM, lo que reducía costes.
  • Memoria mapeada mediante bank switching: muy flexible.
  • Acceso a casi toda la RAM efectiva en la mayoría de programas.
  • Disponía de chips dedicados (VIC‑II y SID), por lo que a nivel de gráficos y sonido era imbatible.

Aunque el Z80 trabajaba a 4 MHz y el 6510 a 1 MHz, el rendimiento real era similar por la arquitectura interna: el Z80 necesitaba entre 3 y 5 ciclos por instrucción; el 6510 solía necesitar 1 o 2. En la práctica, cada máquina tenía puntos fuertes distintos.

Resultado: el C64 era más caro y más potente, con mejor sonido y gráficos. El Spectrum tuvo MUCHOS más juegos, más baratos y más fáciles de desarrollar. Cada usuario tiene su favorito: yo reconozco que el C64 era mejor, pero en mi corazón siempre ganará “mi gomas”.


Variantes

El modelo más conocido es el “panera”, pero hubo muchas versiones:

  • Commodore 64 “Panera” (1982): carcasa ancha beige, teclado mecánico.
  • MAX Machine (1982): un predecesor (por pocos meses), muy simplificado con solo 2 KB de RAM, que no triunfó.
  • Commodore Plus/4 (1984): orientado a ofimática, fracaso comercial en USA, mejor visto en Europa. Incluía en ROM el paquete de los 4 programas clasicos (procesador de texto, base de datos, hoja de cálculo y gráficos), de ahí su nombre..
  • Commodore 128 (1985): Con 128 KB de RAM, disponía de un modo C64 para compatibilizarlo.
  • Commodore 64C: Un rediseño más compacto, con la caja similar al C128.
  • SX-64 / DX‑64: versión portátil con pantalla integrada y una disquetera, una grán máquina.
  • Commodore 64GS: consola sin teclado, solo cartuchos, que no triunfó.
  • Educator 64: versión tipo PET para su uso en escuelas, con la pantalla integrada, que no salió de USA.

Variantes modernas, aunque usando emulación:

  • THEC64 Mini y THEC64 de tamaños mini y completo.
  • Nuevas ediciones modernas como la “Ultimate Edition” (2025).

Máquinas que sucedieron al C64

La familia Amiga

El Amiga 1000 (1985) inauguró la era multimedia real: multitarea, sonido estéreo y gráficos avanzados. La gama creción con varios modelos, como el Amiga 500 que fue el modelo más popular, o el Amiga 2000 el orientado a profesionales.


El final de una era

El C64 se produjo hasta 1994, año de la quiebra de Commodore. Con una vida comercial de 12 años, ventas masivas y más de 10.000 programas, es un caso único en la historia.


Los juegos que definieron una época

Clásicos como The Last Ninja, Impossible Mission, Pitstop II o Maniac Mansion demostraron que con talento 64 KB podían convertirse en mundos memorables.

Muchos juegos en cinta incluían mini‑juegos durante la carga, algo muy característico del C64, que amenizaban la espera mientras cargaban.


La escena actual

El C64 fue muy popular, y hoy día sigue siendolo, por eso la escena se mueve bastante hoy día:

  • Demos que exprimen el hardware más allá de lo imaginable.
  • Nuevos juegos, muchos en físico.
  • Restauraciones y hardware moderno compatible.
  • Recreaciones FPGA de alta fidelidad.



Por qué volver al retro hoy

Encender un C64 hoy es viajar atrás en el tiempo. Una experiencia que recuerda que la creatividad importa más que la potencia bruta. En una era de inmediatez, el C64 nos devuelve a una informática más artesanal, directa… y sorprendentemente relajante.


Fuentes consultadas

  • https://en.wikipedia.org/wiki/Commodore_64
  • https://es.wikipedia.org/wiki/Commodore_Educator_64
  • https://en.wikipedia.org/wiki/ZX_Spectrum
  • https://commodoregaming.com/us-en/Vintage+C64/History.aspx
  • https://retrocomputing.stackexchange.com/questions/17255
  • https://www.commodore.ca/text/c64.htm
  • https://gunkies.org/wiki/Commodore_64
  • https://t-lcarchive.org/sinclair-zx-spectrum/
  • https://www.gamedeveloper.com/business/a-history-of-gaming-platforms-the-commodore-64
  • https://www.youtube.com/watch?v=diacznMip8w
  • https://www.itsitio.com/dispositivos/la-mitica-commodore-64-regresa-modernizada-tres-versiones-preventa-global-y-espiritu-retro/

martes, 17 de marzo de 2026

ZX2SB. Cambios en el planteamiento

Transpilador ZX BASIC a SuperBasic

Índice de entradas del conversor

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



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

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

Vamos a generar dos versiones del transpilador en paralelo

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

1) Versión Moderna

Diseñada para:

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

2) Versión compatible con SuperBasic

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

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

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

¿Por qué dos versiones?

✔ Didáctica

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

✔ Validación de algoritmos

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

✔ Documentación y mantenimiento

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

Pantalla lanzadora

Incluyo una interfaz WinForms que permite seleccionar:

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

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

Arquitectura del proyecto

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

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

Estructura general del proyecto:

ZX2SB

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

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

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

Conclusión

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

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

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

lunes, 16 de marzo de 2026

ZB2SB. Paso 3.2. Lexer. Planteamiento

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

Introducción

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

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

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


1. Analizador léxico

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

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


2. ¿Qué es un token?

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

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

El parser solo opera sobre tokens, nunca sobre caracteres.


3. Tokens que vamos a generar

3.1 Identificadores y variables

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

3.2 Literales

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

3.3 LineNumber

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

3.4 Operadores

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

3.5 Separadores

  • ( ) , ; :

3.6 Palabras clave

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

3.7 Comentarios

  • Comment — tras REM hasta fin de línea

3.8 Control

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

4. Diagrama de bloques del analizador léxico

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

4.3 Notas de implementación

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

5. Lexer en seudo‑código

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

7. El programa generado en Visual Basic .NET

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

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

Notas rápidas

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

domingo, 15 de marzo de 2026

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

Transpilador ZX BASIC para Sinclair QL: Optimización del Lexer (Fase 1)

Índice de entradas del conversor



Introducción

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

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


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

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

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

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

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

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

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

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

3. API de gestión de Tokens

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

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

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

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

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

4. Empaquetado y acceso a los datos del registro

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

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

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

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

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

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

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

5. Alta de nuevos tokens en el arreglo

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

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

6. Prueba: crear registros, recuperarlos y mostrarlos

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

    CrearCasos()

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

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

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

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

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


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

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

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

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

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

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

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

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


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

    fallos ← 0
    hasta ← MIN(TokenCount, NCasos)

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

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

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

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

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

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

7. Siguiente paso

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