Páginas

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

No hay comentarios:

Publicar un comentario