Je travaille sur une installation de complétion (intellisense) pour C # dans emacs.
L'idée est que, si un utilisateur tape un fragment, puis demande la complétion via une combinaison de touches particulière, la fonction de complétion utilisera la réflexion .NET pour déterminer les complétions possibles.
Pour ce faire, il faut que le type de chose en cours de réalisation soit connu. S'il s'agit d'une chaîne, il existe un ensemble connu de méthodes et de propriétés possibles; s'il s'agit d'un Int32, il a un ensemble séparé, et ainsi de suite.
En utilisant sémantique, un package lexer / parser de code disponible dans emacs, je peux localiser les déclarations de variables et leurs types. Compte tenu de cela, il est simple d'utiliser la réflexion pour obtenir les méthodes et les propriétés du type, puis de présenter la liste des options à l'utilisateur. (Ok, ce n'est pas tout à fait simple à faire dans emacs, mais en utilisant la possibilité d'exécuter un processus PowerShell dans emacs , cela devient beaucoup plus facile. J'écris un assembly .NET personnalisé pour faire la réflexion, le charge dans le PowerShell, puis elisp s'exécutant dans emacs peut envoyer des commandes à PowerShell et lire les réponses, via comint. En conséquence, emacs peut obtenir rapidement les résultats de la réflexion.)
Le problème survient lorsque le code utilise var
dans la déclaration de la chose en cours de réalisation. Cela signifie que le type n'est pas spécifié explicitement et que la complétion ne fonctionnera pas.
Comment puis-je déterminer de manière fiable le type réel utilisé, lorsque la variable est déclarée avec le var
mot-clé? Pour être clair, je n'ai pas besoin de le déterminer au moment de l'exécution. Je veux le déterminer au "Design time".
Jusqu'à présent, j'ai ces idées:
- compilez et invoquez:
- extraire l'instruction de déclaration, par exemple `var foo =" une valeur de chaîne ";`
- concaténer une instruction `foo.GetType ();`
- compilez dynamiquement le fragment C # résultant dans un nouvel assembly
- chargez l'assembly dans un nouvel AppDomain, exécutez le framgment et obtenez le type de retour.
- décharger et jeter l'ensemble
Je sais comment faire tout ça. Mais cela semble terriblement lourd, pour chaque demande d'achèvement dans l'éditeur.
Je suppose que je n'ai pas besoin d'un nouvel AppDomain à chaque fois. Je pourrais réutiliser un seul AppDomain pour plusieurs assemblages temporaires et amortir le coût de sa configuration et de sa suppression, sur plusieurs demandes d'achèvement. C'est plus une modification de l'idée de base.
- compiler et inspecter IL
Compilez simplement la déclaration dans un module, puis inspectez l'IL pour déterminer le type réel qui a été déduit par le compilateur. Comment cela serait-il possible? Qu'est-ce que j'utiliserais pour examiner l'IL?
Y a-t-il de meilleures idées là-bas? Commentaires? suggestions?
EDIT - en y réfléchissant davantage, la compilation et l'invocation n'est pas acceptable, car l'invocation peut avoir des effets secondaires. La première option doit donc être écartée.
De plus, je pense que je ne peux pas supposer la présence de .NET 4.0.
MISE À JOUR - La bonne réponse, non mentionnée ci-dessus, mais gentiment soulignée par Eric Lippert, est de mettre en œuvre un système d'inférence de type de fidélité complète. C'est le seul moyen de déterminer de manière fiable le type d'une variable au moment de la conception. Mais ce n'est pas non plus facile à faire. Parce que je ne me fais aucune illusion sur le fait que je veux essayer de construire une telle chose, j'ai pris le raccourci de l'option 2 - extraire le code de déclaration pertinent, le compiler, puis inspecter l'IL résultant.
Cela fonctionne réellement, pour un sous-ensemble équitable des scénarios d'achèvement.
Par exemple, supposons que dans les fragments de code suivants, le? est la position à laquelle l'utilisateur demande la complétion. Cela marche:
var x = "hello there";
x.?
La complétion se rend compte que x est une chaîne et fournit les options appropriées. Pour ce faire, il génère puis compile le code source suivant:
namespace N1 {
static class dmriiann5he { // randomly-generated class name
static void M1 () {
var x = "hello there";
}
}
}
... puis en inspectant l'IL avec une simple réflexion.
Cela fonctionne également:
var x = new XmlDocument();
x.?
Le moteur ajoute les clauses using appropriées au code source généré, de sorte qu'il se compile correctement, puis l'inspection IL est la même.
Cela fonctionne aussi:
var x = "hello";
var y = x.ToCharArray();
var z = y.?
Cela signifie simplement que l'inspection IL doit trouver le type de la troisième variable locale, au lieu de la première.
Et ça:
var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var x = z.?
... qui est juste un niveau plus profond que l'exemple précédent.
Mais ce qui ne fonctionne pas , c'est la complétion sur toute variable locale dont l'initialisation dépend à tout moment d'un membre d'instance ou d'un argument de méthode locale. Comme:
var foo = this.InstanceMethod();
foo.?
Ni la syntaxe LINQ.
Je vais devoir réfléchir à la valeur de ces choses avant d'envisager de les aborder via ce qui est définitivement une "conception limitée" (mot poli pour hack) pour l'achèvement.
Une approche pour résoudre le problème des dépendances sur les arguments de méthode ou les méthodes d'instance serait de remplacer, dans le fragment de code qui est généré, compilé puis analysé par IL, les références à ces choses par des variables locales "synthétiques" du même type.
Une autre mise à jour - la complétion sur les variables qui dépendent des membres de l'instance, fonctionne maintenant.
Ce que j'ai fait, c'est interroger le type (via sémantique), puis générer des membres suppléants synthétiques pour tous les membres existants. Pour un tampon C # comme celui-ci:
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...
... le code généré qui est compilé, afin que je puisse apprendre à partir de la sortie IL le type de la var locale nnn, ressemble à ceci:
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);
}
}
}
Tous les membres de type instance et statique sont disponibles dans le code squelette. Il se compile avec succès. À ce stade, la détermination du type de var local est simple via Reflection.
Ce qui rend cela possible est:
- la possibilité d'exécuter PowerShell dans emacs
- le compilateur C # est vraiment rapide. Sur ma machine, il faut environ 0,5 seconde pour compiler un assemblage en mémoire. Pas assez rapide pour l'analyse entre les frappes, mais assez rapide pour prendre en charge la génération à la demande de listes d'achèvement.
Je n'ai pas encore examiné LINQ.
Ce sera un problème beaucoup plus important car le lexeur / analyseur sémantique emacs a pour C #, ne "fait" pas LINQ.
la source
Réponses:
Je peux vous décrire comment nous faisons cela efficacement dans le "vrai" C # IDE.
La première chose que nous faisons est d'exécuter une passe qui analyse uniquement les éléments de "niveau supérieur" dans le code source. Nous sautons tous les corps de méthode. Cela nous permet de créer rapidement une base de données d'informations sur l'espace de noms, les types et les méthodes (et les constructeurs, etc.) dans le code source du programme. L'analyse de chaque ligne de code dans chaque corps de méthode prendrait beaucoup trop de temps si vous essayez de le faire entre les frappes.
Lorsque l'EDI a besoin de déterminer le type d'une expression particulière à l'intérieur d'un corps de méthode, disons que vous avez tapé «foo». et nous devons comprendre quels sont les membres de foo - nous faisons la même chose; nous sautons autant de travail que nous pouvons raisonnablement.
Nous commençons par une passe qui analyse uniquement les déclarations de variables locales dans cette méthode. Lorsque nous exécutons cette passe, nous effectuons un mappage d'une paire de "scope" et "name" vers un "type determiner". Le "type determiner" est un objet qui représente la notion de "je peux déterminer le type de ce local si j'en ai besoin". Déterminer le type de local peut être coûteux et nous voulons donc reporter ce travail si nécessaire.
Nous avons maintenant une base de données construite paresseusement qui peut nous dire le type de chaque local. Donc, revenons à ce "foo". - nous déterminons dans quelle déclaration se trouve l'expression pertinente, puis nous exécutons l'analyseur sémantique uniquement sur cette déclaration. Par exemple, supposons que vous ayez le corps de la méthode:
et maintenant nous devons comprendre que foo est de type char. Nous construisons une base de données contenant toutes les métadonnées, les méthodes d'extension, les types de code source, etc. Nous construisons une base de données qui a des déterminants de type pour x, y et z. Nous analysons l'énoncé contenant l'expression intéressante. Nous commençons par le transformer syntaxiquement en
Afin de déterminer le type de foo, nous devons d'abord connaître le type de y. Donc, à ce stade, nous demandons au déterminant de type "quel est le type de y"? Il démarre ensuite un évaluateur d'expression qui analyse x.ToCharArray () et demande "quel est le type de x"? Nous avons un déterminant de type pour celui qui dit "Je dois rechercher" String "dans le contexte actuel". Il n'y a pas de type String dans le type actuel, nous regardons donc dans l'espace de noms. Ce n'est pas là non plus, donc nous regardons dans les directives using et découvrons qu'il existe un "using System" et que System a un type String. OK, c'est donc le type de x.
Nous interrogeons ensuite les métadonnées de System.String pour le type de ToCharArray et il dit que c'est un System.Char []. Super. Nous avons donc un type pour y.
Maintenant, nous demandons "System.Char [] a-t-il une méthode Où?" Non. Nous regardons donc dans les directives d'utilisation; nous avons déjà précalculé une base de données contenant toutes les métadonnées des méthodes d'extension qui pourraient éventuellement être utilisées.
Maintenant, nous disons "OK, il y a dix-huit douzaines de méthodes d'extension nommées Où dans la portée, est-ce que l'une d'elles a un premier paramètre formel dont le type est compatible avec System.Char []?" Nous commençons donc une série de tests de convertibilité. Cependant, les méthodes d'extension Where sont génériques , ce qui signifie que nous devons faire une inférence de type.
J'ai écrit un moteur d'inférence de type spécial qui peut gérer les inférences incomplètes du premier argument à une méthode d'extension. Nous exécutons l'inférence de type et découvrons qu'il existe une méthode Where qui prend un
IEnumerable<T>
, et que nous pouvons faire une inférence de System.Char [] àIEnumerable<System.Char>
, donc T est System.Char.La signature de cette méthode est
Where<T>(this IEnumerable<T> items, Func<T, bool> predicate)
, et nous savons que T est System.Char. Nous savons également que le premier argument entre parenthèses de la méthode d'extension est un lambda. Nous démarrons donc un inferrer de type d'expression lambda qui dit «le paramètre formel foo est supposé être System.Char», utilisez ce fait lors de l'analyse du reste du lambda.Nous avons maintenant toutes les informations dont nous avons besoin pour analyser le corps du lambda, qui est "foo". Nous recherchons le type de foo, nous découvrons que selon le classeur lambda c'est System.Char, et nous avons terminé; nous affichons les informations de type pour System.Char.
Et nous faisons tout sauf l'analyse "de haut niveau" entre les frappes . C'est le vrai problème. En fait, écrire toute l'analyse n'est pas difficile; cela le rend suffisamment rapide pour que vous puissiez le faire à une vitesse de frappe qui est la vraie difficulté.
Bonne chance!
la source
Je peux vous dire en gros comment l'EDI Delphi fonctionne avec le compilateur Delphi pour faire de l'intellisense (l'insight code est ce que Delphi l'appelle). Ce n'est pas 100% applicable à C #, mais c'est une approche intéressante qui mérite d'être prise en considération.
La plupart des analyses sémantiques dans Delphi sont effectuées dans l'analyseur lui-même. Les expressions sont tapées au fur et à mesure qu'elles sont analysées, sauf dans les situations où ce n'est pas facile - auquel cas l'analyse par analyse anticipée est utilisée pour déterminer ce qui est prévu, puis cette décision est utilisée dans l'analyse.
L'analyse est en grande partie de descente récursive LL (2), sauf pour les expressions, qui sont analysées en utilisant la priorité des opérateurs. L'une des particularités de Delphi est qu'il s'agit d'un langage à passage unique, donc les constructions doivent être déclarées avant d'être utilisées, donc aucune passe de niveau supérieur n'est nécessaire pour faire ressortir ces informations.
Cette combinaison de fonctionnalités signifie que l'analyseur a à peu près toutes les informations nécessaires à la compréhension du code pour tout point où cela est nécessaire. Voici comment cela fonctionne: l'EDI informe le lexer du compilateur de la position du curseur (le point où la perspicacité du code est souhaitée) et le lexer le transforme en un jeton spécial (il s'appelle le jeton kibitz). Chaque fois que l'analyseur rencontre ce jeton (qui peut être n'importe où), il sait que c'est le signal pour renvoyer toutes les informations dont il dispose à l'éditeur. Il le fait en utilisant un longjmp car il est écrit en C; ce qu'il fait, c'est qu'il informe l'appelant ultime du type de construction syntaxique (c'est-à-dire du contexte grammatical) dans lequel le point de kibitz a été trouvé, ainsi que de toutes les tables symboliques nécessaires pour ce point. Donc par exemple, si le contexte est dans une expression qui est un argument d'une méthode, nous pouvons vérifier les surcharges de méthode, regarder les types d'arguments et filtrer les symboles valides uniquement sur ceux qui peuvent résoudre ce type d'argument (cela réduit beaucoup de cruft non pertinent dans le menu déroulant). Si c'est dans un contexte de portée imbriquée (par exemple après un "."), L'analyseur aura rendu une référence à la portée, et l'EDI peut énumérer tous les symboles trouvés dans cette portée.
D'autres choses sont également faites; par exemple, les corps de méthode sont ignorés si le jeton kibitz ne se trouve pas dans leur plage - ceci est fait de manière optimiste, et annulé s'il a ignoré le jeton. L'équivalent des méthodes d'extension - les assistants de classe en Delphi - ont une sorte de cache versionné, leur recherche est donc raisonnablement rapide. Mais l'inférence de type générique de Delphi est beaucoup plus faible que celle de C #.
Passons maintenant à la question spécifique: inférer les types de variables déclarées avec
var
équivaut à la façon dont Pascal infère le type de constantes. Il provient du type de l'expression d'initialisation. Ces types sont construits de bas en haut. Six
est de typeInteger
, ety
est de typeDouble
, alorsx + y
sera de typeDouble
, car ce sont les règles du langage; etc. Vous suivez ces règles jusqu'à ce que vous ayez un type pour l'expression complète sur le côté droit, et c'est le type que vous utilisez pour le symbole sur la gauche.la source
Si vous ne voulez pas avoir à écrire votre propre analyseur pour construire l'arborescence de syntaxe abstraite, vous pouvez envisager d'utiliser les analyseurs de SharpDevelop ou de MonoDevelop , tous deux open source.
la source
Les systèmes Intellisense représentent généralement le code en utilisant une arborescence de syntaxe abstraite, qui leur permet de résoudre le type de retour de la fonction affectée à la variable «var» plus ou moins de la même manière que le compilateur le fera. Si vous utilisez VS Intellisense, vous remarquerez peut-être qu'il ne vous donnera pas le type de var tant que vous n'aurez pas fini de saisir une expression d'affectation valide (résoluble). Si l'expression est toujours ambiguë (par exemple, elle ne peut pas entièrement déduire les arguments génériques de l'expression), le type var ne sera pas résolu. Cela peut être un processus assez complexe, car vous devrez peut-être marcher assez profondément dans un arbre pour résoudre le type. Par exemple:
Le type de retour est
IEnumerable<Bar>
, mais pour résoudre ce problème, il faut savoir:IEnumerable
.OfType<T>
qui s'applique à IEnumerable.IEnumerable<Foo>
et il existe une méthode d'extensionSelect
qui s'applique à cela.foo => foo.Bar
a le paramètre foo de type Foo. Ceci est déduit par l'utilisation de Select, qui prend unFunc<TIn,TOut>
et puisque TIn est connu (Foo), le type de foo peut être déduit.IEnumerable<TOut>
et TOut peut être déduit du résultat de l'expression lambda, donc le type d'élément résultant doit êtreIEnumerable<Bar>
.la source
Puisque vous ciblez Emacs, il peut être préférable de commencer par la suite CEDET. Tous les détails d'Eric Lippert sont déjà couverts dans l'analyseur de code de l'outil CEDET / Semantic pour C ++. Il y a aussi un analyseur C # (qui a probablement besoin d'un peu de TLC) donc les seules parties manquantes sont liées au réglage des parties nécessaires pour C #.
Les comportements de base sont définis dans des algorithmes de base qui dépendent de fonctions surchargeables définies par langue. Le succès du moteur de complétion dépend de combien de réglages ont été effectués. Avec C ++ comme guide, obtenir un support similaire à C ++ ne devrait pas être trop mauvais.
La réponse de Daniel suggère d'utiliser MonoDevelop pour faire l'analyse et l'analyse. Cela pourrait être un mécanisme alternatif au lieu de l'analyseur C # existant, ou il pourrait être utilisé pour augmenter l'analyseur existant.
la source
var
. Semantic l'identifie correctement comme var, mais ne fournit pas d'inférence de type. Ma question portait précisément sur la façon de résoudre ce problème . J'ai également cherché à me connecter à l'achèvement CEDET existant, mais je ne savais pas comment. La documentation de CEDET est ... ah ... incomplète.C'est un problème difficile à bien faire. Fondamentalement, vous devez modéliser la spécification du langage / compilateur à travers la plupart des lexing / parsing / typechecking et construire un modèle interne du code source que vous pouvez ensuite interroger. Eric le décrit en détail pour C #. Vous pouvez toujours télécharger le code source du compilateur F # (qui fait partie du F # CTP) et jeter un œil à
service.fsi
pour voir l'interface exposée hors du compilateur F # que le service de langage F # utilise pour fournir de l'intellisense, des info-bulles pour les types inférés, etc. un sens d'une «interface» possible si vous aviez déjà le compilateur disponible en tant qu'API à appeler.L'autre méthode consiste à réutiliser les compilateurs tels quels au fur et à mesure que vous les décrivez, puis à utiliser la réflexion ou à examiner le code généré. Ceci est problématique du point de vue que vous avez besoin de `` programmes complets '' pour obtenir une sortie de compilation à partir d'un compilateur, alors que lors de l'édition du code source dans l'éditeur, vous n'avez souvent que des `` programmes partiels '' qui n'analysent pas encore, ne avoir toutes les méthodes encore implémentées, etc.
En bref, je pense que la version «petit budget» est très difficile à bien faire, et la version «réelle» est très, très difficile à bien faire. (Où `` difficile '' mesure ici à la fois `` l'effort '' et la `` difficulté technique ''.)
la source
NRefactory le fera pour vous.
la source
Pour la solution "1", vous disposez d'une nouvelle fonctionnalité dans .NET 4 pour le faire rapidement et facilement. Donc, si vous pouvez convertir votre programme en .NET 4, ce serait votre meilleur choix.
la source