Estoy trabajando en una instalación de finalización (intellisense) para C # en emacs.
La idea es que, si un usuario escribe un fragmento y luego solicita la finalización mediante una combinación de teclas en particular, la función de finalización utilizará la reflexión .NET para determinar las posibles finalizaciones.
Hacer esto requiere que se conozca el tipo de cosa que se está completando. Si es una cadena, hay un conjunto conocido de posibles métodos y propiedades; si es un Int32, tiene un conjunto separado y así sucesivamente.
Usando semántica, un paquete de código lexer / parser disponible en emacs, puedo localizar las declaraciones de variables y sus tipos. Dado eso, es sencillo usar la reflexión para obtener los métodos y propiedades del tipo, y luego presentar la lista de opciones al usuario. (Ok, no es muy sencillo de hacer dentro de emacs, pero usando la capacidad de ejecutar un proceso de PowerShell dentro de emacs , se vuelve mucho más fácil. Escribo un ensamblaje .NET personalizado para hacer la reflexión, lo cargo en el PowerShell y luego elisp se ejecuta dentro emacs puede enviar comandos a powershell y leer respuestas, a través de comint. Como resultado, emacs puede obtener los resultados de la reflexión rápidamente).
El problema llega cuando el código se usa var
en la declaración de lo que se está completando. Eso significa que el tipo no se especifica explícitamente y la finalización no funcionará.
¿Cómo puedo determinar de manera confiable el tipo real utilizado, cuando la variable se declara con la var
palabra clave? Para que quede claro, no necesito determinarlo en tiempo de ejecución. Quiero determinarlo en "Tiempo de diseño".
Hasta ahora tengo estas ideas:
- compilar e invocar:
- extraer la declaración de declaración, por ejemplo, `var foo =" un valor de cadena ";`
- concatenar una sentencia `foo.GetType ();`
- compilar dinámicamente el C # resultante, fragmentarlo en un nuevo ensamblado
- cargue el ensamblado en un nuevo AppDomain, ejecute el fragmento y obtenga el tipo de retorno.
- descargar y desechar el conjunto
Sé cómo hacer todo esto. Pero suena terriblemente pesado, para cada solicitud de finalización en el editor.
Supongo que no necesito un AppDomain nuevo cada vez. Podría reutilizar un solo AppDomain para múltiples ensamblajes temporales y amortizar el costo de configurarlo y desmontarlo, en múltiples solicitudes de finalización. Eso es más una modificación de la idea básica.
- compilar e inspeccionar IL
Simplemente compile la declaración en un módulo y luego inspeccione el IL para determinar el tipo real que fue inferido por el compilador. ¿Cómo sería esto posible? ¿Qué usaría para examinar el IL?
¿Alguna idea mejor por ahí? Comentarios sugerencias?
EDITAR : pensando en esto más a fondo, compilar e invocar no es aceptable, porque la invocación puede tener efectos secundarios. Por tanto, hay que descartar la primera opción.
Además, creo que no puedo asumir la presencia de .NET 4.0.
ACTUALIZACIÓN : la respuesta correcta, que no se mencionó anteriormente, pero que Eric Lippert señaló con delicadeza, es implementar un sistema de inferencia de tipo de fidelidad total. Es la única forma de determinar de forma fiable el tipo de var en tiempo de diseño. Pero tampoco es fácil de hacer. Debido a que no me hago ilusiones de querer intentar construir tal cosa, tomé el atajo de la opción 2: extraer el código de declaración relevante y compilarlo, luego inspeccionar el IL resultante.
Esto realmente funciona, para un subconjunto justo de escenarios de finalización.
Por ejemplo, suponga que en los siguientes fragmentos de código, el? es la posición en la que el usuario solicita la finalización. Esto funciona:
var x = "hello there";
x.?
La finalización se da cuenta de que x es una cadena y proporciona las opciones adecuadas. Lo hace generando y luego compilando el siguiente código fuente:
namespace N1 {
static class dmriiann5he { // randomly-generated class name
static void M1 () {
var x = "hello there";
}
}
}
... y luego inspeccionando el IL con una simple reflexión.
Esto también funciona:
var x = new XmlDocument();
x.?
El motor agrega las cláusulas using apropiadas al código fuente generado, de modo que se compile correctamente, y luego la inspección de IL es la misma.
Esto también funciona:
var x = "hello";
var y = x.ToCharArray();
var z = y.?
Simplemente significa que la inspección de IL tiene que encontrar el tipo de la tercera variable local, en lugar de la primera.
Y esto:
var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var x = z.?
... que es solo un nivel más profundo que el ejemplo anterior.
Pero lo que no funciona es la finalización de cualquier variable local cuya inicialización dependa en cualquier punto de un miembro de instancia o argumento del método local. Me gusta:
var foo = this.InstanceMethod();
foo.?
Ni la sintaxis LINQ.
Tendré que pensar en lo valiosas que son esas cosas antes de considerar abordarlas a través de lo que definitivamente es un "diseño limitado" (palabra cortés para hack) para completar.
Un enfoque para abordar el problema de las dependencias de los argumentos del método o de los métodos de instancia sería reemplazar, en el fragmento de código que se genera, se compila y luego se analiza por IL, las referencias a esas cosas con vars locales "sintéticas" del mismo tipo.
Otra actualización : la finalización en vars que dependen de los miembros de la instancia, ahora funciona.
Lo que hice fue interrogar el tipo (mediante semántica) y luego generar miembros sustitutos sintéticos para todos los miembros existentes. Para un búfer de C # como este:
public class CsharpCompletion
{
private static int PrivateStaticField1 = 17;
string InstanceMethod1(int index)
{
...lots of code here...
return result;
}
public void Run(int count)
{
var foo = "this is a string";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var mmm = count + z + CsharpCompletion.PrivateStaticField1;
var nnn = this.InstanceMethod1(mmm);
var fff = nnn.?
...more code here...
... el código generado que se compila, para que pueda aprender del IL de salida el tipo de var nnn local, se ve así:
namespace Nsbwhi0rdami {
class CsharpCompletion {
private static int PrivateStaticField1 = default(int);
string InstanceMethod1(int index) { return default(string); }
void M0zpstti30f4 (int count) {
var foo = "this is a string";
var fred = new System.Collections.Generic.List<String> { foo, foo.Length.ToString() };
var z = fred.Count;
var mmm = count + z + CsharpCompletion.PrivateStaticField1;
var nnn = this.InstanceMethod1(mmm);
}
}
}
Todos los miembros de tipo instancia y estático están disponibles en el código esqueleto. Se compila con éxito. En ese punto, determinar el tipo de var local es sencillo a través de Reflection.
Lo que hace esto posible es:
- la capacidad de ejecutar powershell en emacs
- el compilador de C # es realmente rápido. En mi máquina, se necesitan aproximadamente 0,5 segundos para compilar un ensamblado en memoria. No lo suficientemente rápido para el análisis entre pulsaciones de teclas, pero lo suficientemente rápido como para admitir la generación bajo demanda de listas de finalización.
Todavía no he investigado LINQ.
Eso será un problema mucho mayor porque el lexer / analizador semántico emacs tiene para C #, no "hace" LINQ.
fuente
Respuestas:
Puedo describirle cómo lo hacemos de manera eficiente en el IDE de C # "real".
Lo primero que hacemos es ejecutar una pasada que analiza solo las cosas de "nivel superior" en el código fuente. Omitimos todos los cuerpos del método. Eso nos permite crear rápidamente una base de datos de información sobre qué espacios de nombres, tipos y métodos (y constructores, etc.) se encuentran en el código fuente del programa. Analizar cada línea de código en cada cuerpo de método llevaría demasiado tiempo si intenta hacerlo entre pulsaciones de tecla.
Cuando el IDE necesita determinar el tipo de una expresión en particular dentro del cuerpo de un método, digamos que ha escrito "foo". y tenemos que averiguar quiénes son los miembros de foo; hacemos lo mismo; nos saltamos todo el trabajo que razonablemente podamos.
Comenzamos con un pase que analiza solo las declaraciones de variables locales dentro de ese método. Cuando ejecutamos esa pasada, hacemos un mapeo de un par de "alcance" y "nombre" a un "determinante de tipo". El "determinante de tipo" es un objeto que representa la noción de "Puedo calcular el tipo de este local si lo necesito". Determinar el tipo de local puede ser costoso, por lo que queremos aplazar ese trabajo si es necesario.
Ahora tenemos una base de datos construida de forma perezosa que puede decirnos el tipo de cada local. Entonces, volviendo a ese "foo". - averiguamos en qué enunciado se encuentra la expresión relevante y luego ejecutamos el analizador semántico solo contra ese enunciado. Por ejemplo, suponga que tiene el cuerpo del método:
y ahora tenemos que averiguar que foo es de tipo char. Creamos una base de datos que tiene todos los metadatos, métodos de extensión, tipos de código fuente, etc. Creamos una base de datos que tiene determinantes de tipo para x, y y z. Analizamos el enunciado que contiene la expresión interesante. Empezamos transformándolo sintácticamente a
Para determinar el tipo de foo, primero debemos conocer el tipo de y. Entonces, en este punto, le preguntamos al determinante de tipo "¿cuál es el tipo de y"? Luego inicia un evaluador de expresiones que analiza x.ToCharArray () y pregunta "¿cuál es el tipo de x"? Tenemos un determinante de tipo para eso que dice "Necesito buscar" Cadena "en el contexto actual". No hay ningún tipo String en el tipo actual, por lo que buscamos en el espacio de nombres. Tampoco está allí, así que buscamos en las directivas using y descubrimos que hay un "using System" y que System tiene un tipo String. Bien, ese es el tipo de x.
Luego consultamos los metadatos de System.String para el tipo de ToCharArray y dice que es un System.Char []. Súper. Entonces tenemos un tipo para y.
Ahora preguntamos "¿System.Char [] tiene un método Dónde?" No. Entonces miramos en las directivas using; ya hemos calculado previamente una base de datos que contiene todos los metadatos de los métodos de extensión que podrían utilizarse.
Ahora decimos "OK, hay dieciocho docenas de métodos de extensión llamados Where in scope, ¿alguno de ellos tiene un primer parámetro formal cuyo tipo es compatible con System.Char []?" Entonces comenzamos una ronda de pruebas de convertibilidad. Sin embargo, los métodos de extensión Where son genéricos , lo que significa que tenemos que hacer una inferencia de tipos.
Escribí un motor de inferencia de tipo especial que puede manejar la realización de inferencias incompletas desde el primer argumento hasta un método de extensión. Ejecutamos el inferidor de tipos y descubrimos que hay un método Where que toma an
IEnumerable<T>
, y que podemos hacer una inferencia desde System.Char [] aIEnumerable<System.Char>
, por lo que T es System.Char.La firma de este método es
Where<T>(this IEnumerable<T> items, Func<T, bool> predicate)
, y sabemos que T es System.Char. También sabemos que el primer argumento dentro del paréntesis del método de extensión es una lambda. Así que iniciamos un inferidor de tipo de expresión lambda que dice "se supone que el parámetro formal foo es System.Char", use este hecho cuando analice el resto de lambda.Ahora tenemos toda la información que necesitamos para analizar el cuerpo de la lambda, que es "foo". Buscamos el tipo de foo, descubrimos que según el enlazador lambda es System.Char, y hemos terminado; mostramos información de tipo para System.Char.
Y hacemos todo excepto el análisis de "nivel superior" entre pulsaciones de teclas . Esa es la parte realmente complicada. En realidad, escribir todo el análisis no es difícil; lo hace lo suficientemente rápido como para que pueda hacerlo a la velocidad de escritura, lo que es realmente complicado.
¡Buena suerte!
fuente
Puedo decirles aproximadamente cómo funciona el IDE de Delphi con el compilador de Delphi para hacer intellisense (la información del código es lo que Delphi lo llama). No es 100% aplicable a C #, pero es un enfoque interesante que merece consideración.
La mayor parte del análisis semántico en Delphi se realiza en el propio analizador. Las expresiones se escriben a medida que se analizan, excepto en situaciones en las que esto no es fácil, en cuyo caso se usa el análisis anticipado para determinar lo que se pretende, y luego esa decisión se usa en el análisis.
El análisis es en gran parte descenso recursivo LL (2), excepto para las expresiones, que se analizan utilizando la precedencia del operador. Una de las cosas distintivas de Delphi es que es un lenguaje de un solo paso, por lo que las construcciones deben declararse antes de usarse, por lo que no se necesita un pase de nivel superior para sacar esa información.
Esta combinación de funciones significa que el analizador tiene aproximadamente toda la información necesaria para obtener información sobre el código en cualquier punto donde se necesite. La forma en que funciona es la siguiente: el IDE informa al lexer del compilador de la posición del cursor (el punto donde se desea obtener información sobre el código) y el lexer lo convierte en un token especial (se llama token kibitz). Siempre que el analizador se encuentra con este token (que podría estar en cualquier lugar), sabe que esta es la señal para devolver toda la información que tiene al editor. Hace esto usando un longjmp porque está escrito en C; lo que hace es notificar al llamador final sobre el tipo de construcción sintáctica (es decir, contexto gramatical) en el que se encontró el punto kibitz, así como todas las tablas simbólicas necesarias para ese punto. Así por ejemplo, Si el contexto está en una expresión que es un argumento para un método, podemos verificar las sobrecargas del método, mirar los tipos de argumentos y filtrar los símbolos válidos solo para aquellos que pueden resolver ese tipo de argumento (esto reduce en un mucho cruft irrelevante en el menú desplegable). Si está en un contexto de alcance anidado (por ejemplo, después de un "."), El analizador habrá devuelto una referencia al alcance y el IDE puede enumerar todos los símbolos que se encuentran en ese alcance.
También se hacen otras cosas; por ejemplo, los cuerpos de los métodos se omiten si el token kibitz no se encuentra en su rango; esto se hace de manera optimista y se revierte si se salta el token. El equivalente de los métodos de extensión, los ayudantes de clases en Delphi, tienen una especie de caché versionado, por lo que su búsqueda es razonablemente rápida. Pero la inferencia de tipos genéricos de Delphi es mucho más débil que la de C #.
Ahora, a la pregunta específica: inferir los tipos de variables declaradas con
var
es equivalente a la forma en que Pascal infiere el tipo de constantes. Viene del tipo de expresión de inicialización. Estos tipos se construyen de abajo hacia arriba. Six
es de tipoInteger
, yy
es de tipoDouble
, entoncesx + y
será de tipoDouble
, porque esas son las reglas del lenguaje; etc. Siga estas reglas hasta que tenga un tipo para la expresión completa en el lado derecho, y ese es el tipo que usa para el símbolo de la izquierda.fuente
Si no quiere tener que escribir su propio analizador para construir el árbol de sintaxis abstracta, puede utilizar los analizadores de SharpDevelop o MonoDevelop , ambos de código abierto.
fuente
Los sistemas Intellisense típicamente representan el código usando un árbol de sintaxis abstracta, lo que les permite resolver el tipo de retorno de la función que se asigna a la variable 'var' más o menos de la misma manera que lo hará el compilador. Si usa VS Intellisense, puede notar que no le dará el tipo de var hasta que haya terminado de ingresar una expresión de asignación válida (que se pueda resolver). Si la expresión sigue siendo ambigua (por ejemplo, no puede inferir completamente los argumentos genéricos de la expresión), el tipo var no se resolverá. Este puede ser un proceso bastante complejo, ya que es posible que deba caminar bastante profundo en un árbol para resolver el tipo. Por ejemplo:
El tipo de retorno es
IEnumerable<Bar>
, pero para resolver esto se requiere saber:IEnumerable
.OfType<T>
que se aplica a IEnumerable.IEnumerable<Foo>
y hay un método de extensiónSelect
que se aplica a esto.foo => foo.Bar
tiene el parámetro foo de tipo Foo. Esto se infiere mediante el uso de Select, que toma unFunc<TIn,TOut>
y, dado que se conoce TIn (Foo), se puede inferir el tipo de foo.IEnumerable<TOut>
y TOut se puede inferir del resultado de la expresión lambda, por lo que el tipo de elementos resultante debe serIEnumerable<Bar>
.fuente
Dado que está apuntando a Emacs, puede ser mejor comenzar con la suite CEDET. Todos los detalles de Eric Lippert están cubiertos en el analizador de código en la herramienta CEDET / Semantic para C ++ ya. También hay un analizador de C # (que probablemente necesite un poco de TLC), por lo que las únicas partes que faltan están relacionadas con el ajuste de las partes necesarias para C #.
Los comportamientos básicos se definen en algoritmos centrales que dependen de funciones sobrecargables que se definen por idioma. El éxito del motor de finalización depende de la cantidad de ajustes que se hayan realizado. Con c ++ como guía, obtener soporte similar a C ++ no debería ser tan malo.
La respuesta de Daniel sugiere usar MonoDevelop para realizar análisis y análisis. Este podría ser un mecanismo alternativo en lugar del analizador de C # existente, o podría usarse para aumentar el analizador existente.
fuente
var
. Semantic lo identifica correctamente como var, pero no proporciona inferencia de tipo. Mi pregunta giraba específicamente en torno a cómo abordar eso . También busqué conectarme a la terminación de CEDET existente, pero no pude averiguar cómo. La documentación de CEDET está ... ah ... no está completa.Es un problema difícil hacerlo bien. Básicamente, necesita modelar la especificación / compilador del lenguaje a través de la mayor parte del lexing / parsing / typechecking y construir un modelo interno del código fuente que luego puede consultar. Eric lo describe en detalle para C #. Siempre puede descargar el código fuente del compilador de F # (parte del CTP de F #) y echarle un vistazo
service.fsi
para ver la interfaz expuesta del compilador de F # que consume el servicio de lenguaje F # para proporcionar intellisense, información sobre herramientas para tipos inferidos, etc. un sentido de una posible 'interfaz' si ya tenía el compilador disponible como una API para llamar.La otra ruta es reutilizar los compiladores tal como están como está describiendo y luego usar la reflexión o mirar el código generado. Esto es problemático desde el punto de vista de que necesita 'programas completos' para obtener una salida de compilación de un compilador, mientras que al editar el código fuente en el editor, a menudo solo tiene 'programas parciales' que aún no analizan, no tener implementados todos los métodos, etc.
En resumen, creo que la versión de 'bajo presupuesto' es muy difícil de hacer bien, y la versión 'real' es muy, muy difícil de hacer bien. (Donde 'difícil' aquí mide tanto 'esfuerzo' como 'dificultad técnica').
fuente
NRefactory hará esto por usted.
fuente
Para la solución "1", tiene una nueva función en .NET 4 para hacer esto rápida y fácilmente. Entonces, si puede convertir su programa a .NET 4, sería su mejor opción.
fuente