viernes, 17 de febrero de 2017

Reescribiendo PSeInt (parte 2.a): arquitectura

Ya introduje en el primer post de esta serie los problemas que busco resolver en esta travesía de reescribir desde cero la parte más crítica de PSeInt. Ahora toca hablar un poco más en detalle de la nueva arquitectura interna de esta "parte".

La tarea es: analizar el pseudocódigo que ingresa el usuario, tanto como sea necesario (pero no más). El análisis más demandante llegará hasta interpretarlo. Pero hay otras funcionalidades como el indentado o el autocompletado que solo requieren una pequeña fracción de ese análisis, y sería entonces deseable que puedan ejecutar solo esa fracción y no deban pagar por lo que no usan. Además, quiero mantener las etapas del análisis relativamente desacopladas para que en el futuro sea fácil modificar alguna (para buscar y corregir errores, o para agregar cosas como por ej. registros :).

El modelo al que arrivé tiene 4 etapas, a las que llamaré por ahora (1) "Tokenizado" (o "Análisis por línea"), (2) "Análisis de instrucción", (3) "Ensamblado del algoritmo" y (4) "Ejecución".

Figura tomada del post anterior: ejemplo para las nuevas etapas. 
Nota: todavía no se si "graficar" y "exportar" requieren la última etapa.

El tokenizado consiste en cortar las partecitas fundamentales de una linea de pseudocódigo. Es decir, cortar espacios, palabras clave, operadores, operandos, constantes, caracteres especiales, etc. La idea sería convertir el string en un arreglo de "tokens" ya clasificados. Al hacerlo, ahora evito normalizar el contenido de la linea, para perder menos información en las conversiones y marcar con más detalles los errores. Esta etapa no mira si las partes se unen formando algo coherente o no, simplemente las corta. Solo puedo detectar aquí errores muy simples como que en una línea falten cerrar comillas, pero no mucho más.

Luego el análisis de instrucción toma la lista de tokens y trata de darle algo de sentido. Primero identifica qué sublistas corresponden a instrucciones, y a cuáles; y por cada una tratará de identificar sus partes lógicas. Por ejemplo, si es un "Para", tratará de determinar cual es la variable de control, cual el valor inicial, el valor final y el paso si existe. Aquí entonces puedo identificar errores en el conformado de una instrucción individual, y si no los hay mandar a procesar la instrucción, ya habiendo hecho el trabajo de separar sus partes/argumentos.

El análisis por instrucción mira instrucciones separadas, sin contexto. El "Para" del ejemplo anterior, aunque esté bien formado, podría ser un error si está fuera de un proceso o de una función, o si no tiene más abajo su correspondiente "Fin Para". Aquí entra en juego el "ensamblado". En esta etapa se reciben las instrucciones ya cocinadas y se arman las funciones, los procesos y las estructuras de control, verificando que las piezas encajen con lógica.

Finalmente, el ensamblado habrá generado funciones y procesos, que dentro tendrán identificadas instrucciones y estructuras de control, y las estructuras recursivamente tendrán más instrucciones y más estructuras también perfectamente identificadas. Ahora solo queda ejecutar.

Entonces, cada etapa tiene una responsabilidad muy clara y única. El tokenizado debe distinguir los tipos de tokens, el análisis por instrucción debe estructurar las instrucciones, el ensamblado debe combinarlas en procesos y funciones, y finalmente la ejecución será quien realmente las use para algo.

Algunos usos alternativos podrían ser:
  • Autocompleatdo: ejecutar solo el tokenizado, y solo hasta encontrar la palabra clave que define la instrucción actual, para saber así el contexto y mostrar opciones acorde.
  • Indentado: ejecutar solo el tokenizado para encontrar solo las palabras claves que generan un cambio en el nivel de indentado de una línea a otra.
  • Diagrama de flujo: reemplazar el ensamblado por uno alternativo que construya una representación gráfica del algoritmo.
  • Exportar a otros lenguajes: también se podría reemplazar la etapa de ensamblado; o tal vez la etapa de ejecución ya que la traducción puede requerir información de contexto más allá de una linea.
En azul, el camino habitual hasta la ejecución. En verde, posibles caminos alternativos para otras funcionalidades.
En rojo, la generación de errores. Cada etapa del análisis puede identificar diferentes tipos de errores.

Y así irán apareciendo otras opciones. Por ejemplo, las opciones del menú contextual del editor para definir una variable, o también para renombrarla, requieren de cierto análisis.

Pero lo más interesante creo que está en la forma de implementar estas etapas para que no queden tan acopladas y que la cosa sea eficiente, tanto cuando se realiza el proceso completo hasta la ejecución, como cuando se interrumpe a la mitad para el autocompletado o algo similar. Más detalles de este y otros problemas en el próximo post, la segunda parte de la segunda parte.

3 comentarios:

  1. No logro comprender en detalle lo que decís, pero te deseo éxitos, en mi humilde opinión me parece un proyecto más que interesante. Éxitos!!!

    ResponderEliminar
  2. Yo tampoco entiendo mucho, pero toca leer más para ver si aclaro dudas. Muchas gracias.

    ResponderEliminar
  3. Yo tampoco entiendo mucho, pero toca leer más para ver si aclaro dudas. Muchas gracias.

    ResponderEliminar