martes, 7 de marzo de 2017

Reescribiendo PSeInt (parte 2.c): complicaciones

Si estuviera haciendo un intérprete para un lenguaje propio y para un uso real, seguramente tendría cuidado de ajustar el lenguaje para que sea simple de parsear. Esto es bueno por mil razones, pero la directa es facilitarle la vida al intérprete y a todas las demás herramientas que surjan junto al lenguaje. Es la clase de cosas que hacen que en un lenguaje como C++ algo tan simple como renombrar un método sea una pesadilla, mientras que en otros lenguajes más nuevos como Java en verdad es simple. En fin, volviendo al pseudocódigo, el pseudolenguaje que usamos no es de los súper simples para este punto de vista, y eso complica un poquito el nuevo diseño del intérprete.

Mi idea era separar claramente las etapas que mencioné antes:
  • La primera era la que separaba una línea de código en partecitas o "tokens". Idealmente, una línea como "Escribir A+90;" se separaría en 5 tokens: 1) Palabra clave "Escribir", 2) Identificador "A", 3) Operador "+", 4) Constante numérica "90", 5) Separador de instrucciones ";". 
  • A partir de eso, el análisis por instrucción detectaría fácilmente que el primer token es una palabra clave, y al observar que la palabra clave es "Escribir" ya determinaría de qué instrucción se trata y que lo que queda por delante hasta el punto y coma o hasta el fin de linea son los argumentos (lista de expresiones a mostrar en pantalla). 
Entonces, ambas etapas pueden ir separadas. El tokenizado solo corta la cadena y en realidad no le importa mucho qué instrucción o palabra clave sea, ni qué forma deberían tener los argumentos. No le importa para nada la gramática del lenguaje.
  • Luego el análisis posterior de la instrucción toma por entrada la lista de tokens ya individualizados y clasificados, y así no tiene que lidiar nunca directamente con el código fuente.

Peeero... Pequeños detalles como que algunas palabras clave son en realidad "frases" clave complican las cosas. Digo "frases" porque se componen por más de una palabra. Por ejemplo, si mi perfil permite ambos tipos de "Para", el de siempre (controlado por un índice numérico, "Para i desde 1 hasta 10 ..."), y el de arreglos (que recorre los elementos con una referencia, "Para Cada Elemento de V ...."), entonces determinar que la primer palabra es "Para" no alcanza para determinar la instrucción. Y de hecho, sería deseable que el tokenizado me devuleva directamente el token: Palabra clave "Para Cada". Sé que en este ejemplo es simple agregar un poquito de lógica al análisis de la instrucción con el fin de desambigüar en la segunda etapa, pero no siempre es tan directo. Y además, la solución no sería general si quiero permitir que cada uno configure sus palabras o frases clave a gusto (y sí quiero).

Para peor, algunas palabras pueden serlo o no en función del contexto. Esta regla es discutible, pero creo que necesaria para que no termine siendo todo palabra clave y no quede nada para los nombres de variables. Si veo un "sin" en un "Escribir" no se si es la variable "sin" o parte de la construcción "Sin Saltar" (o "Sin Bajar", que se usa para que no se agregue un Enter al final de la linea). Tengo que seguir analizando lo que sigue, y si eso no es "Saltar" o "Bajar", volver hacia atrás. Y dado que mi "tokenizador" funcionaba como un stream que produce bajo demanda, no es gratis lo de "volver hacia atrás". Tampoco es caro (uso un memento ultra liviano), pero es que es otro detalle más a considerar.

Entonces, mi solución por el momento es que el tokenizador no distinga identificadores de palabras claves... que solo corte "palabras", y que sea tarea de la etapa siguiente hacer la distinción. La etapa siguiente aún necesitará avanzar por las dudas en el stream de tokens y eventualmente volver hacia atrás, así que eso no se simplifica; pero sí se simplifica el tema de las palabras claves dependientes del contexto, ya que la segunda etapa es la que define cuál es ese contexto.


Otro caso similar ocurre cuando lo primero que extraemos de una línea no indica si es o no una instrucción válida. Como en la asignación. Lo primero sería un identificador de variable. ¿Lo segundo debería ser la flecha de asignación? No necesariamente. Podrían venir índices si el identificador se un arreglo, campos si es un registro (cuando pueda serlo en el futuro), o más combinaciones. Entonces nuevamente hay que avanzar bastante para decidir y eventualmente retroceder.


En definitiva, lo que ocurre es que el producto final termina siendo algo más complejo de lo que parecía el diseño inicial. Por ahora ninguna solución encarece notablemente el funcionamiento, ni en tiempo ni en memoria. Son entonces manejables con pequeños compromisos. Pero el punto es que la idea base tan simple pierde un poquito de generalidad al empezar a agregar pequeños tratamientos especiales aquí y allá. Tienen que ser pocos y fácilmente identificables para que el nuevo diseño, que promete simplificar todo, no se vuelva a complicar solo. Por suerte, por ahora lo son, y pareciera que no faltan muchos. Creo que vamos bien.

1 comentario:

  1. ¿No consideraste usar expresiones regulares o algo como "pattern matching" para detectar el tipo de instrucción?

    ResponderEliminar