Uso de un Analizador Léxico

Antes de ver cómo se construye un analizador léxico, en este apartado se verá cómo se usaría uno que ya estuviera construido para mostrar, así, la funcionalidad que deberá tener. Se verá cómo devuelve los tokens y qué deja en cada uno.

Representación de los Tokens

Se ha visto que un token no es más que un entero. Se había visto, además, que dichos números se definían como constantes para no tener que estar recordando el valor numérico asignado a cada uno. Estas constantes son las que se utilizarán en los posteriores ejemplos y no los valores numéricos que representen.

De hecho, hay una simplificación cuando el token está formado por un sólo lexema y dicho lexema tiene sólo un carácter. En este caso, como número, se le asocia a dicho token el código ASCII de dicho carácter.

Por ejemplo, en el siguiente caso, en los tokens IGUAL y P_COMA (punto y coma), dado que se les podría asignar cualquier número, se les asigna concretamente sus propios códigos ASCII.







 
 



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

    static final int IGUAL ==;
    static final int P_COMA =;;

}

El hacer esto permite una simplificación del código: dado que su número es conocido, ya no es necesario definir dichas constantes por ser redundante.







 
 


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

    // static final int IGUAL = ‘=’;
    // static final int P_COMA = ‘;’;
}

Cuando un código posterior quiera referirse, por ejemplo, al token IGUAL, dado que sabe que su entero será la constante '=', pondrá directamente dicho carácter en vez de definir una constante artificial para él.

Debido a esto es por lo que los demás tokens se suelen numerar a partir del valor 256. De esta manera se asegura que no colisionen con los tokens definidos mediante un carácter.

Implementación de un Token

Aunque hay varias formas de hacerlo, en esta asignatura se usará la siguiente forma de representar un token en código:

public class Token {

    public Token(String lexeme, int tokenType, int line, int column) {
        this.lexeme = lexeme;
        this.tokenType = tokenType;
        this.line = line;
        this.column = column;
    }

    public String getLexeme() { return lexeme; }
    public int getType() { return tokenType; }
    public int getLine() { return line; }
    public int getColumn() { return column; }

    private String lexeme;
    private int tokenType, line, column;
}

Por cada lexema de la entrada, se creará un objeto de la clase Token anterior el cual contendrá cuatro elementos:

  • El lexema reconocido.
  • La tipo (categoría) del lexema (tokentType).
  • La posición del fichero (línea y columna) donde se encontró el lexema. Esto es importante debido a que las demás fases ya no tratarán con el fichero de entrada. Si éstas necesitan saber dónde se encontraba el lexema (por ejemplo, para notificar un error), sólo pueden acceder a su posición original si el léxico la ha facilitado (ya que es la única fase que trata con la entrada).

Nota

📌 Nótese que el término token se está utilizando para dos cosas:

  • Por un lado, es la categoría en la que se ha clasificado el lexema (en el constructor de Token, sería el segundo parámetro).
  • Por otro lado, también se está llamando token al objeto que encapsula tanto el lexema como su categoría (lo que en el punto anterior se acaba de definir como clase Token).

Por tanto, un token ¿es sólo la categoría del lexema o es la unión del lexema y su categoría? Realmente se utiliza indistintamente para ambas cosas. Sin embargo, en la práctica, esto no suele producir confusión ya que, en función del contexto, normalmente será evidente saber a cual de las dos cosas se está refiriendo dicho término.

Entrega de los Tokens

Un analizador léxico suele implementarse como una clase:

class Lexico {
    ...

}

Se ha mencionado anteriormente que el analizador léxico devuelve una serie de parejas de lexemas y tokens (objetos de la clase Token anterior). Pero ¿dónde deja dichos objetos?

Aunque en la visión general de las fases de un traductor se presenta cada fase como una tarea que acaba antes de que empiece la siguiente, el análisis léxico no se suele hacer de esta manera (aunque se podría). Es decir, el analizador léxico no realiza todo su trabajo y, al acabar, devuelve una lista con todos los tokens. Esto supondría un gasto de memoria innecesario, ya que realmente no se necesita tener a la vez todos los tokens en memoria.

En lugar de ello, se suele tener un método que, cada vez que se le invoque, devuelve sólo el siguiente token del fichero. Por tanto, el análisis léxico está entrelazado con el análisis sintáctico. Cada vez que el sintáctico necesita un token, llama al léxico, el cual lee los caracteres necesarios para formar el siguiente lexema y, una vez obtenido, vuelve al sintáctico sin leer más caracteres de la entrada (que dejará para llamadas posteriores)

Por tanto, la clase Léxico tiene un método similar al siguiente:







 


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

    Token nextToken() {} // Sólo devuelve un Token cada vez
}

Hace falta, por tanto, una forma de que el léxico indique que ya no quedan más tokens a la entrada (para que se deje de llamar a dicho método). Por convenio, cuando ya no hay más tokens, se devuelve un token END (que, aunque no todas las herramientas lo cumplen, suele ser el valor cero). Dicho token se define junto con los demás tokens:


 







class Lexico {
    static final int END = 0;
    static final int IDENT = 256;
    static final int WHILE = 257;
    static final int IF = 260;
    ...

}

Ejemplo 0220

Como resumen de lo anterior, y para ver qué hace el léxico como una caja negra (antes de que en apartados posteriores se vea cómo lo hace), se mostrará un ejemplo de qué hace el analizador léxico ante una entrada.

Supóngase un programa cuyo bucle principal de ejecución va invocando a nextToken y, a continuación, imprime una traza del contenido del objeto Token obtenido en cada llamada:

Lexico lex = new Lexico(new FileReader(“prog.txt"));
Token token;
while ((token = lex.nextToken()).getType() != Lexico.END) {
    System.out.println(token.getLexeme());
    System.out.println(token.getType());
    System.out.println(token.getLine());
}

Supóngase la siguiente entrada contenida en el fichero prog.txt:

altura
+
1.75

Y finalmente, supóngase que se han definidos las siguientes constantes asociadas a los tokens del lenguaje:

class Lexico {
    static final int END = 0;
    static final int IDENT = 257;
    static final int IF = 258;
    static final int LITENT = 259;
    static final int LITREAL = 260;}

El resultado de ejecutar dicho programa sería el siguiente, en el que cada fila representa una iteración del bucle.

IteracióngetLexeme()getType()getLine()
1"altura"2571
2"+"43 ('+')2
3"1.75"2603