Especificación Léxica

En el capítulo anterior se ha mostrado cómo se usa un analizador léxico ya hecho. En los siguientes capítulos se verá cómo se hace uno.

Pero antes de empezar a escribir código, hay que tener claro qué se va a hacer; hace falta tener previamente los planos. Los planos del léxico es lo que se denomina especificación léxica. Los pasos para obtener esta especificación son dos:

Una especificación léxica es el resultado de juntar lo obtenido en los dos pasos anteriores; no es más que una relación de tokens junto con el patrón de cada uno.

A continuación, se mostrará cada paso del proceso en mayor detalle.

1. Determinar Tokens

Se ha comentado que los lexemas se agrupan en tokens (categorías), pero ¿cuántos tokens/categorías se necesitan para agrupar todos los lexemas del lenguaje? ¿Cuál es el criterio para juntar o separar los lexemas en una misma categoría?

Por ejemplo, para las llaves y los paréntesis habría dos opciones:

  1. Juntar todos estos lexemas en una sola categoría/token DELIMITADOR:





     


    class Lexico {
        static final int IDENT = 256;
        static final int WHILE = 257;
        ...
    
        static final int DELIMITADOR = 261; // Lexemas: '(' ')' '{' '}'
    }
    
  2. Hacer una categoría por cada lexema.





     
     
     
     


    class Lexico {
        static final int IDENT = 256;
        static final int WHILE = 257;
        ...
    
        static final int PARENT_A =(;
        static final int PARENT_C =);
        static final int LLAVE_A ={;
        static final int LLAVE_C =};
    }
    

¿Cuál es el criterio para hacerlo de una manera o de otra? Para ello, hay que definir de una forma más clara lo que representa un token.

Definición

📖 Un token es el conjunto de lexemas que pueden ser tratadas como una unidad sintáctica.

Por tanto, hay que crea un token con todos aquellos lexemas que el analizador sintáctico (la fase siguiente) vaya a tratar de la misma manera; es decir, que al analizador sintáctico le sea indiferente cual de los lexemas del token aparezca, ya que no va a afectar a la estructura sintáctica a reconocer. Si esto ocurre, los lexemas pertenecen al mismo token. Si no es así, deben estar en tokens distintos.

Para aclarar lo anterior, supóngase la siguiente regla sintáctica de un lenguaje ficticio que establece la estructura de una asignación, es decir, el orden en el que deben aparecer las partes de dicha sentencia:

asignacion: IDENT '=' NUMERO

En la regla anterior, al sintáctico le da igual cómo se llame la variable que aparezca a la izquierda del '='. Por tanto, todos los nombres de variables podrán agruparse en el mismo token IDENT (identificador). De la misma manera, todas las secuencias de dígitos pueden agruparse en el token NUMERO. No hay ciertos números que sean válidos a la derecha de la asignación y otros no.

Volviendo a la duda anterior sobre cómo representar los delimitadores ¿ocurre lo mismo con los las llaves y los paréntesis? ¿Se pueden tratar por igual? No, ya que no son intercambiables. Por ejemplo, supóngase la regla sintáctica que indica cómo se accede a un array:

array: IDENT '[' expr ']'

En el ejemplo anterior, en una entrada válida, no podría aparecer el lexema ')' en lugar del '['. Para acceder a un array tienen que usarse corchetes y, concretamente, tiene que ser primero el de abrir y luego el de cerrar. Por tanto, como los delimitadores anteriores no pueden tratarse por igual (se quiere distinguir cada uno de ellos - no son intercambiables), no pueden juntarse en un mismo token.

En definitiva, la solución correcta para los delimitadores es hacer un token por cada uno:






 
 
 
 


class Lexico {
    static final int IDENT = 256;
    static final int WHILE = 257;
    ...

    static final int PARENT_A =(;
    static final int PARENT_C =);
    static final int LLAVE_A ={;
    static final int LLAVE_C =};
}

Una vez llegados a esta solución, lo más práctico es aplicar lo comentado en el capítulo anterior sobre la representación de los tokens. Dado que esos cuatro son tokens con un sólo lexema de un sólo carácter, en realidad no sería necesario definirlos.

Para acabar, podría plantearse la duda de que, dado que el agrupar lexemas en tokens o no depende de lo que necesite el analizador sintáctico ¿cómo es posible hacer el léxico si todavía no se ha hecho el sintáctico y no se sabe lo que necesitará? En la práctica esto no suele ser un problema ya que esto suele ser fácil de prever. En cualquier caso, es posible que, a la hora de hacer el analizador sintáctico, haya que reajustar ciertos tokens en el léxico.

Pregunta

⏳ ¿Debería crearse un sólo token OPERADORES para los lexemas '+', '-', '*' y '/' o bien un token distinto para cada uno?

2. Definir Patrones

Una vez definidos todos los tokens, hay que dejar claro qué lexemas pertenecen a cada uno y cuáles no. Por ejemplo, supóngase la siguiente relación de lexemas:

_a
hol#a
_32

¿Són lexemas válidos? ¿Són identificadores (IDENT)?

No se puede saber sin tener más información. Depende de lo que haya decidido el creador del lenguaje. Por tanto, se necesitan reglas que indiquen qué situaciones són válidas y cuáles no. Y dentro de las válidas, saber a qué categoría/token pertenecen. Esto es lo que hace el patrón de un token; es la regla que indica qué lexemas pertenecen a dicho token.

En el ejemplo anterior, el autor del lenguaje deberá indicar, mediante un patrón, qué es un identificador válido y, así, se podrá saber si _a es un lexema válido o no.

Pero, ¿en qué notación se expresan los patrones? Con metalenguajes.