sábado, 14 de marzo de 2026

ZB2SB. Paso Auxiliar 1. Estructuras de datos

Transpilador ZX BASIC — Estructuras de datos para el transpilador (y adaptación a SuperBasic)

Índice de entradas del conversor


Introducción

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

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

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

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

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


1. Por qué necesitamos estructuras propias

SuperBasic no dispone de:

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

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

2. Qué es un registro

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

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

2.1 Campos dentro de un registro

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

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

Todos esos campos juntos forman un solo registro Token.

2.2 Cómo representaremos un registro

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

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

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

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

2.3 Formatos de empaquetado para registros

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

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

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

3. Almacenamiento de cadenas en el QL real

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

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

Cuando declaramos un arreglo de cadenas como:

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

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

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

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

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

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


4. Comparativa de modelos de estructuras en SuperBasic

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

4.1 Varios arreglos paralelos

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

4.2 Arreglo multidimensional

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

4.3 Un único arreglo de cadena con los datos empaquetados

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

5. Estructuras de datos del transpilador

Las estructuras necesarias son:

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

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


6. Descripción detallada de cada estructura

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


6.1 Tokens (Lexer)

6.1.1 Representación

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

TokenData$(n)
TokenCount
TokenMax

6.1.2 Registro: TokenRecord

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

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

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

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

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

6.1.3 Empaquetado

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

6.2 AST por línea (Parser)

6.2.1 Representación

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

6.2.2 Nodo de Línea

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

6.2.3 Sentencias

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

6.2.4 Expresiones

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

6.3 Tabla de Líneas (Semántico)

6.3.1 Representación

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

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

6.3.2 Registro: LineRecord

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

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

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

6.4 Tabla de Símbolos

6.4.1 Representación

Un único arreglo de cadenas con separadores:

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

6.4.2 Registro: SymbolRecord

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

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

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

6.5 Diagnósticos

6.5.1 Representación

Un único arreglo de cadenas con separadores:

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

6.5.2 Registro: DiagRecord

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

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

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

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

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

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

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

6.6 Buffer de salida

6.6.1 Representación

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

Output$(n)    = lineaSuperBasic$
OutputCount
OutputMax

6.6.2 Registro: OutputRecord

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

7. Seudo‑código de las estructuras

7.0 Funciones genéricas para usar arreglos como lista

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

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

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

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

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

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

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

    Max ← nuevoTamaño
FIN

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

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

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

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

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

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

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

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

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

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

PROCEDIMIENTO Token_Iniciar()
    Estructura_Iniciar(TokenMax, TokenCount)
FIN

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

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

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

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

8. Siguientes pasos

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

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

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


No hay comentarios:

Publicar un comentario