Índice de entradas del conversor
📘 Tabla de Contenidos
- Introducción
- 1. Por qué necesitamos estructuras propias
- 2. Qué es un registro
- 2.1 Campos dentro de un registro
- 2.2 Cómo representaremos un registro
- 2.3 Formatos de empaquetado para registros
- 3. Almacenamiento de cadenas en el QL real
- 3.1 ¿Qué hay realmente en un arreglo de cadenas?
- 4. Comparativa de modelos de estructuras en SuperBasic
- 5. Estructuras de datos del transpilador
- 6. Descripción detallada de cada estructura
- 7. Seudo‑código de las estructuras
- 8. Siguientes pasos
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
nidentifica 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