Hace un tiempo que me decidí a reescribir este último mecanismo. Lo quise reescribir porque estaba fragmentado en diferentes lugares de ZinjaI y entonces había algunas funcionalidades repetidas, y otras difíciles de reutilizar. Mi idea era mejorar el diseño para agregar algunas mejoras muy útiles, y de paso tratar de que todo se analice con el mismo código para simplificar el mantenimiento y acotar los problemas. En este artículo entonces voy a describir los problemas a los que me enfrento al hacer esto, y qué hay de nuevo para la próxima versión de ZinjaI.
Imaginen por ejemplo que el usuario ingresa un punto. Si lo que había antes es un objeto, tengo que mostrar la lista de métodos (si era parte de un flotante por ejemplo no). Entonces podría extraer la palabra que hay antes del punto, usar el primer mecanismo propio para averiguar su tipo y preguntarle al parser si conoce esa clase para que me diga sus métodos. Pero esa palabra podría ser un atributo de otra clase, entonces hay que ver qué hay antes de esa palabra, y así sucesivamente. En otros casos (no en el del punto), podría no ser un objeto, sino una clase, un puntero a función, el identificador de una macro de preprocesador, un namespace, etc. Pero esos casos también podrían identificarse, y seguir con esta idea de ir de atrás para adelante. Porque ir de atrás para adelante, aunque no da garantías de correctitud de ningún tipo, es mucho más rápido que analizar todo desde el comienzo.
El primer problema aparece con los operadores: corchetes y asteriscos, etc. Por ejemplo: ¿qué pasa cuando me encuentro un paréntesis o un signo de mayor? El paréntesis me puede hacer pensar que lo que había antes era una función, entonces busco la palabra antes del paréntesis para ver cual es. Pero también puede ser un constructor o un cast; o también puede ser simplemente una expresión entre paréntesis, y no importar lo que había antes. Con el signo mayor pasa algo similar: puedo indicar el final del argumento de un template, o el operador mayor, o el de redirección de flujo/desplazamiento de bits, etc. Y distinguir los casos del mayor es mucho más complicado que los casos de los paréntesis, porque involucra saber si lo que tiene al lado son operandos. Pero esos operando pueden ser expresiones, y las expresiones pueden involucrar casteos implícitos, argumentos por defecto en llamadas a funciones, sobrecarga de operadores, etc. Entonces el análisis se complica mucho mucho.
Ejemplo:
#define cadena string // macro simple (id=tipo)#define saludo string("Hola Mundo") // macro compleja (id=expresion)
string bah() { ... } // función que retorna string
class Foo { // clase con operadores que retornan string
...
string operator()(int,float) { ... }
string operator[](int x) { ... }
};
template<class T> struct Templ { // clase templatizada
...
T t1;
};
int mee(string *r, string &bla, int i) {
Foo f(1,2.5); // para sobrecarga de operadores
Templ<string> temp; // clase genérica
vector<string> v(10,"A"); // vetor stl
cadena cad; // cadena es una macro, equivale a string
// al final de todas estas lineas debiera mostrarse el
// autocompletado con los métodos de string
// las que ya funcionaban y siguen funcionando
string:: // operador de scope
bla. // objeto
cad. // macro simple
r-> // puntero
r[2]. // arreglo
(*r+i). // arreglo
(bla+"hola"). // expresión entre paréntesis
// las que no funcionaban y ahora sí funcionan en casos simples
bah(). // llamada a función
bla.replace(1,3,"*"). // llamada a método
string("adfsd"). // constructor
f(). // sobrecarga del ()
f[3]. // sobrecarga del []
// las que siguen sin funcionar
saludo. // macro compleja
temp.t1. // template
v[i]. // template
Templ<string>.t1. // template
...
}
Entre estos ejemplos hay 7 casos que ya se resolvían correctamente, 5 casos en los que
el sistema de autocompletado ha mejorado, y 4 que siguen sin funcionar.
el sistema de autocompletado ha mejorado, y 4 que siguen sin funcionar.
ZinjaI ahora tiene un código para cuando cree que hay un tipo de dato, otro para cuando cree que hay una función o método, otro para cuando espera un objeto, y todos ellos tienen tareas comunes y repetidas. Por eso quiero unificar, y hacer un sólo código que me diga qué hay, sin suponer nada (porque muchas veces las suposiciones son falsas). Pero hacerlo bien implica lo que dije antes, que nos llevaría a parsear todo (hasta el contenido de los includes), y sería lento y complicado. Tan complicado que sería mejor dejárselo a un compilador. Pero esto tampoco es bueno. Alguna vez mencioné que podía usar clang para que me cante la justa. Pero eso solo funciona si el código es correcto, y mientras el usuario edita el programa, no es correcto. Van a faltar includes, puntos y comas, va a haber errores de tipeo, etc. Por eso insisto con una heurística propia. Al no poder ni querer manejar tanta complejidad, el autocompletado mentirá o fallará en casos complicados. Pero al basarse en reglas heurísticas, al guiarse por las apariencias, será más tolerante a errores y muchísimo más rápido que las otras opciones.
Ese siempre fue el objetivo. Solo que "casos complicados" abarcaba demasiadas cosas. Siempre pienso en el estudiante. Si el autocompletado le miente a un programador experimentado, este se da cuenta y lo ignora. Si el autocompletado le miente a un estudiante, lo confunde. Aunque por otro lado, el estudiante no intenta hacer construcciones tan raras adrede, por lo que podría conformarme con que ande bien en los casos no tan "complicados". Pero hasta ahora había casos muy comunes para el usuario que para el autocompletado eran complicados. Algunos son difíciles de evitar, a pesar de ser increíblemente comunes. Por ejemplo, si tengo un "vector<string> v", al escribir "v[i]." debería autocompletar con los métodos de la clase string, pero detectar eso implica ser consiente del template, de su especialización y de la sobrecarga del operador []. Tres cosas que ZinjaI hasta ahora no sabía (por limitaciones tanto propias como de cbrowser). A partir de ahora, por lo menos entederá una de esas tres, la sobrecarga del operador. De forma similar ocurre con las funciones que devuelven objetos. Si hay sobrecargas, parámetros por defecto, conversiones implícitas en las llamadas, etc, la cosa se complica. Pero si hay una sola, o todas devuelven lo mismo por ejemplo, todo ese análisis no es necesario, y la respuesta es bastante fácil.
En definitiva, estos son los casos que van a mejorar en la próxima versión. Ya tengo algunos implementados y los estoy probando para luego publicarlos. ZinjaI va a poder determinar los tipos de retorno de las llamadas a funciones y la utilización de sobrecarga de algunos operadores particulares en los casos más simples. Por suerte esos son casos mucho más frecuentes que los complicados, y me consta que pueden marcar una buena diferencia. El gran talón de Aquiles que resta por mejorar está en el uso de templates (algo para nada menor), y que con la evolución de C++11 se ha vuelto todavía más útil y todavía más complicado. Por esa clase de cosas, no reestructuré por completo el mecanismo actual tanto como planeaba, sino que me detuve a analizar otras opciones muy diferentes que quedarán para discutir en la parte 3.
No hay comentarios:
Publicar un comentario