Este proyecto es una implementación de un analizador léxico y sintáctico basado en Autómatas Finitos Determinísticos (AFD), Autómatas de Pila Determinista (APD) y un parser LL(1). Está diseñado para analizar un lenguaje de programación personalizado que incluye declaraciones de variables, asignaciones, expresiones, condicionales y una función imprime.
- El scanner genera tokens de uno en uno conforme se leen los caracteres del código.
- El parser identifica qué autómata o parser secundario debe ser llamado para procesar los siguientes tokens.
- Si el autómata o parser secundario reconoce la estructura, el parser principal continúa con el siguiente token.
Puede ver el proyecto en funcionamiento en este enlace.
Si desea ejecutar el proyecto de manera local, siga estos pasos:
- Clone el repositorio en su máquina local:
git clone https://github.com/divorcedlance/scanner-parser.git
- Acceda al directorio del proyecto:
cd scanner-parser
- Abra el archivo
index.html
en su navegador. Nota: Puede que necesite utilizar un servidor local para que el JavaScript se cargue correctamente. Puede usar una extensión como "Live Server" en Visual Studio Code o cualquier otro servidor web local que prefiera.
La interfaz de usuario se divide en dos secciones principales: el área de código y el área de salida. El área de código se utiliza para ingresar el código fuente y el área de salida se utiliza para mostrar los resultados del análisis. Además, se tienen botones para usar códigos de ejemplo, para copiar al portapapeles el código del editor y para copiar el output en la salida.
El área de código tiene un área de texto que se puede utilizar para ingresar el código fuente. También tiene 2 botones uno para copiar el código fuente y otro para usar un ejemplo de código aleatorio.
El área de salida tiene un área de texto que se utiliza para mostrar los resultados del análisis. También tiene un botón para copiar el texto de salida al portapapeles.
El proceso de análisis de código se ejecuta a través de la función run
, que guía el flujo del código a través de varias etapas, desde su entrada hasta la producción de un resultado.
- El scanner genera uno por uno tokens a partir del código dado. Cada token tiene un tipo y un valor, además de la información para ubicarlo en el código fuente.
- Si el token es desconocido, se genera un error léxico.
- Si se llega al final del código fuente, se genera un token especial
EOF
que indica el final del archivo.
- El parser identifica qué autómata o parser secundario debe ser llamado para procesar los tokens.
- Cuando se encuentra un token inicial, se llama al AFD o APD correspondiente para evaluar la secuencia de tokens.
- En el caso de los AFD se usa el método
evaluarAFD
para evaluar la secuencia de tokens. Este método genera un error si la secuencia no sigue las reglas del AFD. Además en el caso de que en el estado actual tengamos una transición valida usando una expresión se usará el parser secundario LL(1) a través del métodoevaluarExpresion
para evaluarla. - En el caso del APD se usa el método
evaluarAPD
para evaluar la secuencia de tokens. El APD se encarga principalmente de las estructuras de control, como "si", "sino", "mientras" y sus respectivas finalizaciones. Este método genera un error si la secuencia no sigue las reglas del APD. Además en el caso de que la estructura requiera una condicional se llamará alAFD4
para evaluar la condicional.
- En el caso de los AFD se usa el método
- Si el AFD o APD reconoce la estructura, el parser principal continúa con el siguiente token como inicial.
- Si en algún momento se genera un error, el parser principal se detiene, se muestra el mensaje de error y el cursor del editor va al token que ocasionó el error.
- Si el parser principal llega al final del código fuente sin errores, se muestra un mensaje de éxito.
El scanner, también conocido como analizador léxico, es responsable de transformar el código fuente en una secuencia de tokens, que se utilizan posteriormente en el análisis sintáctico.
Cuando se crea un objeto de la clase Scanner
, se inicializan varios atributos:
code
: El código fuente que se analizará.TokenType
: Una enumeración de los diferentes tipos de tokens que se pueden encontrar.palabrasReservadas
: Un listado de palabras que el lenguaje reconoce como reservadas.linea
: Un contador que mantiene el seguimiento de la línea actual del código fuente.tokenGenerator
: Una referencia al generador que produce tokens uno a uno.
Para convertir el código en tokens, utilizamos un generador que itera sobre cada carácter del código fuente:
El procedimiento es sencillo:
- Se itera sobre cada carácter.
- Si se encuentra un salto de línea, se aumenta el contador de líneas y se clasifica el lexema si existe.
- Si se encuentra un espacio, símbolo u operador, se clasifica el lexema acumulado y se reinicia. En el caso de símbolos u operandos, estos también se clasifican.
- Si se encuentra una letra o un número, se añade al lexema.
- Cualquier otro carácter se trata como desconocido.
Una vez que se ha identificado un lexema, es necesario determinar a qué tipo de token pertenece. Esto se logra a través del método clasificarLexema
:
El método verifica si el lexema es:
- Una palabra reservada.
- Un identificador válido.
- Un número (entero o decimal).
- Un operador.
- Un símbolo.
- O cualquier otro carácter desconocido.
Si se encuentra un lexema desconocido, se muestra un mensaje de error utilizando la función displayError
.
El método getToken
permite obtener el siguiente token del código fuente. Internamente, utiliza el generador para obtener tokens y manejar casos especiales, como múltiples saltos de línea consecutivos:
Al final del código fuente, se devuelve un token especial EOF
que indica el final del archivo.
La clase Parser
está diseñada para analizar la sintaxis de un lenguaje de programación utilizando varias técnicas, como Autómatas Finitos Deterministas (AFD), Autómatas de Pila Determinista (APD) y un parser LL(1). El parser se encarga de decidir qué técnica utilizar en base al token inicial de una nueva estructura.
si
,sino
,fsi
,mientras
,fmientras
: Gestionados por APD.enter
,real
: Gestionados por AFD1.imprime
: Gestionado por AFD2.
- Gestionados por AFD3.
Estos se utilizan para reconocer estructuras simples como declaraciones de variables, la función imprime
, asignaciones y condicionales.
Estado inicial:
Estados finales:
this.AFD1 = {
'name': 'declaración de variables',
'q0': { 'entero': 'q1', 'real': 'q1' },
'q1': { 'ID': 'q2' },
'q2': { ',': 'q1', '=': 'q3', '\n': 'qf', 'EOF': 'qf' },
'q3': { 'EXP': 'q4' },
'q4': { ',': 'q1', '\n': 'qf', 'EOF': 'qf' },
'estado_final': ['qf']
};
Estado inicial:
Estados finales:
this.AFD2 = {
'name': 'funcion imprime',
'q0': { 'imprime': 'q1' },
'q1': { 'EXP': 'q2' },
'q2': { ',': 'q1', '\n': 'qf', 'EOF': 'qf' },
'estado_final': ['qf']
};
Estado inicial:
Estados finales:
this.AFD3 = {
'name': 'asignación',
'q0': { 'ID': 'q1' },
'q1': { '=': 'q2' },
'q2': { 'EXP': 'q3' },
'q3': { '\n': 'qf', 'EOF': 'qf' },
'estado_final': ['qf']
};
Estado inicial:
this.AFD4 = {
'name': 'condición',
'q0': { '(': 'q1' },
'q1': { 'EXP': 'q2' },
'q2': { '<': 'q3', '>': 'q3' },
'q3': { 'EXP': 'q4' },
'q4': { ')': 'q5' },
'q5': { '\n': 'qf' },
'estado_final': ['qf']
};
Estos se emplean para analizar estructuras de control, como si
, sino
y mientras
.
Estado inicial:
this.APD = {
'q0': {
'si': { 'P0': { 'estado': 'q0', 'pila': '+' }, '-': { 'estado': 'q0', 'pila': '+' } },
'sino': { 'si': { 'estado': 'q0', 'pila': '+' } },
'fsi': { 'sino': { 'estado': 'q0', 'pila': '&&' }, 'si': { 'estado': 'q0', 'pila': '&' } },
'mientras': { 'P0': { 'estado': 'q0', 'pila': '+' }, '-': { 'estado': 'q0', 'pila': '+' } },
'fmientras': { 'mientras': { 'estado': 'q0', 'pila': '&' } }
}
};
Se usa para analizar expresiones matemáticas, identificando estructuras como términos, factores y operadores.
Para las expresiones algebraicas usaremos la gramática
Símbolos No Terminales:
$N = {E, T, F, G}$
Símbolos Terminales:
$T_e = {+, -, *, /, \ \hat{ \ } , (, ), \text{ID}, \text{NUM}}$
Reglas de Producción:
Para evitar ambigüedades y preparar nuestra gramática para el análisis LL(1), hemos factorizado la gramática original:
Los conjuntos de símbolos directores nos ayudan a tomar decisiones en el análisis sintáctico descendente, indicando cuándo aplicar una regla particular:
Con esto ya podemos implementar el parser LL(1) en nuestro código:
evaluarExpresion() {
// Verifica que la expresión sea válida utilizando el parser LL(1)
try {
this.E();
} catch (error) {
return false;
}
return true;
}
// Método E
E() {
this.T();
this.X();
}
// Método X
X() {
switch (this.currentToken.value) {
case '+':
this.match('+');
this.E();
break;
case '-':
this.match('-');
this.E();
break;
case 'EOF':
case '\n':
case ',':
case '<':
case '>':
case ')':
// Lambda
break;
default:
throw new Error(`Token innesperado en X: "${this.currentToken.value}"`);
}
}
// Método T
T() {
this.F();
this.Y();
}
// Método Y
Y() {
switch (this.currentToken.value) {
case '*':
this.match('*');
this.T();
break;
case '/':
this.match('/');
this.T();
break;
case '+':
case '-':
case 'EOF':
case '\n':
case ',':
case '<':
case '>':
case ')':
// Lambda
break;
default:
throw new Error(`Token innesperado en Y: "${this.currentToken.value}"`);
}
}
// Método F
F() {
this.G();
this.Z();
}
// Método Z
Z() {
switch (this.currentToken.value) {
case '^':
this.match('^');
this.F();
break;
case '*':
case '/':
case '+':
case '-':
case 'EOF':
case '\n':
case ',':
case '<':
case '>':
case ')':
// Lambda
break;
default:
throw new Error(`Token innesperado en Z: "${this.currentToken.value}"`);
}
}
// Método G
G() {
switch (this.currentToken.value) {
case '(':
this.match('(');
this.E();
this.match(')');
break;
case 'ID':
this.match('ID');
break;
case 'NUM':
this.match('NUM');
break;
default:
throw new Error(`Token innesperado en G: "${this.currentToken.value}"`);
}
}
- Inicializa los diferentes autómatas (AFD y APD) y otras propiedades.
- Obtiene el siguiente token del escáner (
scanner
) y lo almacena encurrentToken
.
- Toma un AFD como argumento y evalúa la secuencia de tokens.
- Si la secuencia no sigue las reglas del AFD, genera un error.
- Evalúa el APD para estructuras de control.
- Si no hay una transición definida en el APD basada en el estado actual y el token, genera un error.
- Modifica la pila del APD según las operaciones definidas.
- Evalúa la corrección sintáctica de una expresión usando el parser LL(1).
- Representan la gramática del parser LL(1) y analizan las expresiones.
- Función principal del analizador.
- Itera a través de los tokens y, basándose en el token actual, decide qué técnica (AFD, APD, LL(1)) usar.
El proyecto se organiza en múltiples archivos y módulos para facilitar la separación de responsabilidades y la mantenibilidad del código:
index.html
: Interfaz gráfica del usuario para la entrada del código y visualización de resultados.styles.css
: Hoja de estilos para la interfaz gráfica.app.js
: Contiene la función principal para ejecutar el compilador y manejar la interfaz de usuario.scanner.js
: Contiene la implementación del analizador léxico.parser.js
: Implementa el analizador sintáctico y decide qué autómata llamar.error.js
: Contiene la implementación del sistema de manejo de errores.example_code.js
: Contiene una lista de ejemplos de código que se pueden utilizar para probar el compilador.
Esta función se invoca cada vez que se presiona el botón "Ejecutar" o se utiliza el atajo Ctrl+Enter
. Coordina la ejecución del analizador léxico y sintáctico y actualiza la salida en la interfaz de usuario.
Se utilizan eventos DOM para gestionar la interacción con el usuario, como la actualización de números de línea en tiempo real y el desplazamiento del área de texto.
Se proporcionan funciones para copiar el código y la salida al portapapeles del usuario, mejorando así la usabilidad del proyecto.
Se proporciona una lista de ejemplos de código que se pueden utilizar para probar el compilador.
El proyecto tiene un sistema robusto para el manejo de errores, que proporciona mensajes de error detallados que especifican el tipo y la línea del error. El sistema de manejo de errores se implementa en el archivo error.js
y se utiliza en el scanner y el parser. Este no solo permite imprimir el error sino también saltar a la ubicación del token que lo generó en el código fuente.