jueves, 5 de abril de 2012

El autocompletado y yo (capítulo 1)

Si hay algo que hace cosas raras en ZinjaI es el autocompletado. A veces funciona, a veces no, a veces se confunde, y otras inventa. Un buen programador sabrá notar sus defectos. Por eso voy a intentar, con una serie de artículos, justificar algunos con sus virtudes, contarles porqué tomé estas decisiones a la hora de diseñarlo, y de paso explicar en lineas generales cómo funciona, qué hace bien, qué hace mal, qué no hace, y qué planes tengo para mejorarlo.

La culpa de todo la tienen Basic y Borland C++ Builder. Como ya comenté antes en algún que otro post, aprendí a programar con Basic y jugué con eso por muchos años. Y allí no había autocompletado alguno (a menos que llamemos a los atajos de teclado que escribían palabras clave autocompletado, pero no es lo mismo). De hecho en los primeros Basics que usé no había siquiera un "editor" (entre comillas porque es discutible), hasta que descubrí QBasic y QuickBasic. Es por esto que en mis comienzos no estaba acostumbrado al autocompletado, y entonces no tenía mucho con que comparar cuando usaba uno como para opinar sobre sus virtudes y defectos. Pero en Builder, que es lo que usábamos en la facultad cuando aprendí C++, había algo que cualquiera podía notar aún sin saber nada del asunto: era desesperantemente lento. Las PCs de los laboratorios de la universidad no eran aviones ni mucho menos, pero tampoco estaban tan mal para ese momento. Y aún así, cuando uno ponía un punto después de un objeto, o la flecha después de un puntero (cosa que ocurre cada 3 segundos mientras se programa en C++), podía irse a tomar un café y volver al rato a ver si había terminado de cargar el menú de autocompletado. Y esto me sacaba de quicio, claro está. Así que cuando empecé a pensar en el autocompletado de ZinjaI, solo impuse una condición: tenía que ser rápido.

Autocompletar correctamente es un arte oscuro; pero autocompletar de forma útil (que no es lo mismo) es aún más oscuro. La especificación de C++ no ayuda ni siquiera un poquito (otros lenguajes, como Java, son mucho más fáciles de analizar, hasta diría que eso ha de haber sido un objetivo a la hora de diseñarlo). Para hacerlo con todas las de la ley habría que parsear absolutamente todo. Es decir, si quiero autocompletar algo en la línea 10, tengo que analizar el código de esa línea, todas las anteriores, todos los archivos que incluye y los archivos que incluyen los incluidos, y probablemente deba mirar para abajo también (si estoy en una clase por ejemplo). Y cuando digo analizar es analizar en detalle, identificar todo, desde el principio, porque las cosas en C++ pueden no ser lo que parecen. Hay código de compilación condicional (#ifdef y demás), las cabeceras pueden incluirse dos veces con distintas configuraciones, las opciones en la linea de comandos cuando se llama al compilador pueden cambiarlo todo, una misma palabra significa cosas totalmente diferentes en archivos distintos, o hasta en distintos puntos del mismo archivo, están los espacios de nombres y la directiva using, están los templates, las sobrecargas, las conversiones de tipo implícitas... y las macros de preprocesador. Estas macros son terriblemente útiles, no podría vivir sin ellas, pero son la pesadilla de un parser. Nada es lo que parece, las macros pueden ofuscar el código, hacer que lo que parece una función sea una variable, o lo que parecía la declaración de un objeto sea cualquier otra cosa.

Además, supongamos que hago un parser perfecto que analiza un googol de lineas en un periquete. No sirve. Si dije que ZinjaI tenía que ser amigable para el estudiante, estoy diciendo que tiene que ser tolerante frente a errores u omisiones. Esto significa que hay que parsear código con errores, o el ejemplo más claro, que hay que autocompletar con símbolos declarados en cabeceras que no están incluidas todavía, porque el alumno no sabe en qué cabecera está cada cosa cuando empieza (y aún sabiendo, es cómodo escribir sin preocuparse, y después agregar lo que falte, o pedirle al IDE que lo haga). Entonces necesito un parseo ultraveloz y ultraliviano con alta tolerancia a errores y que además esté al tanto de todo lo que el código no dice pero podría querer decir, leyendo si es posible la mente del usuario y corrigiendo sus errores. Decidí que eso es imposible, y empecé sacrificando la correctitud en pos de la velocidad, basándome en la siguiente premisa: las cosas que el alumno hace no debieran ser taan raras (no usa macros, no anida templates, etc, o al menos no intencionalmente), y el usuario que sí hace cosas difíciles adrede las hace porque sabe y no se va a dejar confundir por un autocompletado con trastornos de personalidad.

Por otro lado, separé el análisis en dos partes. Uno serio, y uno rápido. Para el serio, tomé un parser de otro proyecto de software libre (cbrowser, de source navigator), para el rápido implementé una lógica propia (y para nada lógica). El parser serio lo uso cuando se abre o se guarda un archivo, momento en donde se manda a analizar en detalle y se extraen de él todos los símbolos importantes (que van a parar al árbol de símbolos). El parser propio es rápido porque analiza sólo el código abierto, aprovechando el etiquetado que ya hizo scintilla (el control que colorea la sintaxis), y buscando de atrás para adelante para evitar leer todo, tratando de inferir y adivinar cosas en el camino. Este lo uso para saber qué es lo que está escrito (por ejemplo, de qué tipo es un objeto para el cual tengo que mostrar la lista de atributos y métodos). Una vez determinado qué es lo que hay, se busca en la base de datos generada por cbrowser con qué autocompletarlo. Pero además, se cargan indices con definiciones adicionales, que armé de antemano, y que contienen las cosas definidas en las cabeceras estándar, o en bibliotecas externas. Esto es necesario porque no es práctico parsear todos los includes y sus includes recursivamente (ni el parser serio lo hace). De esta forma, funciona como un cache de simbolos que se carga al iniciar ZinjaI y contiene las cosas que no están definidas en el código, pero podrían aparecer mediante #includes.

En el próximo capítulo comentaré en más detalle como almaceno la información que brinda cbrowser y en qué consiste esta información, y cómo realizo ese parseo veloz hacia atrás, para que se entiendan mejor las limitaciones, y los posibles caminos para mejorarlo.


No hay comentarios:

Publicar un comentario