📘 Tabla de Contenidos
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
4.3 Notas de implementación
- Normalizar CRLF/CR a
\n. - Keywords: case‑insensitive, se normalizan a MAYÚSCULAS.
LineNumbersolo en columna 1.- REM: dos variantes —
REMvacío oREM␠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