Wie kann ich den Typ einer Variablen, die zur Entwurfszeit mit var deklariert wird, zuverlässig bestimmen?

109

Ich arbeite an einer Vervollständigungsfunktion (Intellisense) für C # in Emacs.

Die Idee ist, dass, wenn ein Benutzer ein Fragment eingibt und dann über eine bestimmte Tastenkombination nach Abschluss fragt, die Abschlussfunktion .NET Reflection verwendet, um die möglichen Abschlüsse zu ermitteln.

Um dies zu tun, muss der Typ der zu erledigenden Sache bekannt sein. Wenn es sich um eine Zeichenfolge handelt, sind eine Reihe möglicher Methoden und Eigenschaften bekannt. Wenn es sich um ein Int32 handelt, hat es einen separaten Satz und so weiter.

Mit semantic, einem in emacs verfügbaren Code-Lexer / Parser-Paket, kann ich die Variablendeklarationen und ihre Typen finden. Angesichts dessen ist es einfach, Reflection zu verwenden, um die Methoden und Eigenschaften des Typs abzurufen und dem Benutzer dann die Liste der Optionen zu präsentieren. (Ok, nicht ganz einfach zu tun , innerhalb von Emacs, wobei jedoch die Möglichkeit , einen Powershell - Prozess innerhalb von Emacs zu laufen , wird es viel einfacher. Ich ein benutzerdefinierte .NET schreibe Montag Reflexion zu tun, es in die Powershell laden, und dann elisp im laufenden emacs können über comint Befehle an Powershell senden und Antworten lesen. Dadurch können emacs die Ergebnisse der Reflexion schnell abrufen.)

Das Problem tritt auf, wenn der Code varin der Deklaration der abgeschlossenen Sache verwendet wird. Das bedeutet, dass der Typ nicht explizit angegeben ist und die Vervollständigung nicht funktioniert.

Wie kann ich den tatsächlich verwendeten Typ zuverlässig bestimmen, wenn die Variable mit dem varSchlüsselwort deklariert wird? Um ganz klar zu sein, muss ich es zur Laufzeit nicht ermitteln. Ich möchte es zur "Entwurfszeit" bestimmen.

Bisher habe ich folgende Ideen:

  1. kompilieren und aufrufen:
    • Extrahieren Sie die Deklarationsanweisung, z. B. "var foo =" a string value ";"
    • verketten Sie eine Anweisung `foo.GetType ();`
    • Kompilieren Sie das resultierende C # -Fragment dynamisch in eine neue Assembly
    • Laden Sie die Assembly in eine neue AppDomain, führen Sie das Framgement aus und rufen Sie den Rückgabetyp ab.
    • Entladen und entsorgen Sie die Baugruppe

    Ich weiß, wie man das alles macht. Aber es klingt für jede Abschlussanforderung im Editor furchtbar schwer.

    Ich brauche wohl nicht jedes Mal eine neue AppDomain. Ich könnte eine einzelne AppDomain für mehrere temporäre Assemblys wiederverwenden und die Kosten für das Einrichten und Herunterfahren über mehrere Abschlussanforderungen amortisieren. Das ist eher eine Optimierung der Grundidee.

  2. IL kompilieren und inspizieren

    Kompilieren Sie einfach die Deklaration in ein Modul und überprüfen Sie dann die IL, um den tatsächlichen Typ zu bestimmen, der vom Compiler abgeleitet wurde. Wie wäre das möglich? Womit würde ich die IL untersuchen?

Irgendwelche besseren Ideen da draußen? Bemerkungen? Vorschläge?


BEARBEITEN - Wenn Sie weiter darüber nachdenken, ist das Kompilieren und Aufrufen nicht akzeptabel, da der Aufruf Nebenwirkungen haben kann. Die erste Option muss also ausgeschlossen werden.

Außerdem kann ich das Vorhandensein von .NET 4.0 nicht annehmen.


UPDATE - Die richtige Antwort, die oben nicht erwähnt wurde, auf die Eric Lippert jedoch sanft hingewiesen hat, besteht darin, ein Inferenzsystem vom Typ Full Fidelity zu implementieren. Dies ist die einzige Möglichkeit, den Typ einer Variable zur Entwurfszeit zuverlässig zu bestimmen. Aber es ist auch nicht einfach zu tun. Da ich keine Illusionen habe, dass ich versuchen möchte, so etwas zu erstellen, habe ich die Verknüpfung von Option 2 gewählt: Extrahieren Sie den entsprechenden Deklarationscode, kompilieren Sie ihn und überprüfen Sie die resultierende IL.

Dies funktioniert tatsächlich für eine angemessene Teilmenge der Abschlussszenarien.

Angenommen, in den folgenden Codefragmenten ist das? ist die Position, an der der Benutzer nach Abschluss fragt. Das funktioniert:

var x = "hello there"; 
x.?

Die Vervollständigung erkennt, dass x ein String ist, und bietet die entsprechenden Optionen. Dazu wird der folgende Quellcode generiert und anschließend kompiliert:

namespace N1 {
  static class dmriiann5he { // randomly-generated class name
    static void M1 () {
       var x = "hello there"; 
    }
  }
}

... und dann die IL mit einfacher Reflexion inspizieren.

Das funktioniert auch:

var x = new XmlDocument();
x.? 

Die Engine fügt dem generierten Quellcode die entsprechenden using-Klauseln hinzu, damit er ordnungsgemäß kompiliert wird. Anschließend ist die IL-Überprüfung dieselbe.

Das funktioniert auch:

var x = "hello"; 
var y = x.ToCharArray();    
var z = y.?

Es bedeutet nur, dass die IL-Inspektion den Typ der dritten lokalen Variablen anstelle der ersten finden muss.

Und das:

var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
    {
        foo,
        foo.Length.ToString()
    };
var z = fred.Count;
var x = z.?

... das ist nur eine Ebene tiefer als das vorherige Beispiel.

Was jedoch nicht funktioniert, ist die Vervollständigung einer lokalen Variablen, deren Initialisierung zu einem beliebigen Zeitpunkt von einem Instanzmitglied oder einem lokalen Methodenargument abhängt. Mögen:

var foo = this.InstanceMethod();
foo.?

Noch LINQ-Syntax.

Ich muss darüber nachdenken, wie wertvoll diese Dinge sind, bevor ich darüber nachdenke, sie über ein definitiv "begrenztes Design" (höfliches Wort für Hack) zur Vervollständigung anzusprechen.

Ein Ansatz zur Behebung des Problems mit Abhängigkeiten von Methodenargumenten oder Instanzmethoden besteht darin, in dem Codefragment, das generiert, kompiliert und anschließend IL analysiert wird, die Verweise auf diese Dinge durch "synthetische" lokale Variablen des gleichen Typs zu ersetzen.


Ein weiteres Update - die Fertigstellung von Variablen, die von Instanzmitgliedern abhängen, funktioniert jetzt.

Ich habe den Typ (über die Semantik) abgefragt und dann synthetische Ersatzmitglieder für alle vorhandenen Mitglieder generiert. Für einen C # -Puffer wie diesen:

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...

... der generierte Code, der kompiliert wird, damit ich aus der Ausgabe IL den Typ der lokalen Variable nnn lernen kann, sieht folgendermaßen aus:

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);
      }
  }
}

Alle Instanz- und statischen Typelemente sind im Skeleton-Code verfügbar. Es wird erfolgreich kompiliert. Zu diesem Zeitpunkt ist die Bestimmung des Typs der lokalen Variable über Reflection unkompliziert.

Was dies möglich macht, ist:

  • die Fähigkeit, Powershell in Emacs auszuführen
  • Der C # -Compiler ist sehr schnell. Auf meinem Computer dauert das Kompilieren einer In-Memory-Assembly etwa 0,5 Sekunden. Nicht schnell genug für die Analyse zwischen Tastenanschlägen, aber schnell genug, um die On-Demand-Erstellung von Abschlusslisten zu unterstützen.

Ich habe mich noch nicht mit LINQ befasst.
Das wird ein viel größeres Problem sein, da die semantischen Lexer / Parser-Emacs für C # LINQ nicht "tun".

Cheeso
quelle
4
Der Typ von foo wird vom Compiler durch Typinferenz herausgefunden und ausgefüllt. Ich vermute, die Mechanismen sind völlig anders. Vielleicht hat die Typ-Inferenz-Engine einen Haken? Zumindest würde ich 'Typ-Inferenz' als Tag verwenden.
George Mauer
3
Ihre Technik, ein "falsches" Objektmodell zu erstellen, das alle Typen, aber keine der Semantiken der realen Objekte aufweist, ist gut. So habe ich damals IntelliSense für JScript in Visual InterDev gemacht. Wir erstellen eine "gefälschte" Version des IE-Objektmodells, die alle Methoden und Typen, aber keine der Nebenwirkungen aufweist, und führen dann beim Kompilieren einen kleinen Interpreter über den analysierten Code aus, um festzustellen, welcher Typ zurückkommt.
Eric Lippert

Antworten:

202

Ich kann für Sie beschreiben, wie wir das in der "echten" C # IDE effizient machen.

Das erste, was wir tun, ist einen Pass auszuführen, der nur das "Top Level" -Ding im Quellcode analysiert. Wir überspringen alle Methodenkörper. Auf diese Weise können wir schnell eine Datenbank mit Informationen darüber aufbauen, welche Namespaces, Typen und Methoden (und Konstruktoren usw.) im Quellcode des Programms enthalten sind. Das Analysieren jeder einzelnen Codezeile in jedem Methodenkörper würde viel zu lange dauern, wenn Sie versuchen, dies zwischen Tastenanschlägen auszuführen.

Wenn die IDE den Typ eines bestimmten Ausdrucks in einem Methodenkörper ermitteln muss, geben Sie "foo" ein. und wir müssen herausfinden, was die Mitglieder von foo sind - wir tun dasselbe; Wir überspringen so viel Arbeit wie möglich.

Wir beginnen mit einem Durchlauf, der nur die lokalen Variablendeklarationen innerhalb dieser Methode analysiert . Wenn wir diesen Durchgang ausführen, machen wir eine Zuordnung von einem Paar aus "Bereich" und "Name" zu einem "Typbestimmer". Der "Typbestimmer" ist ein Objekt, das den Begriff "Ich kann den Typ dieses Lokals ermitteln, wenn ich muss" darstellt. Das Ausarbeiten des Typs eines Einheimischen kann teuer sein, daher möchten wir diese Arbeit verschieben, wenn dies erforderlich ist.

Wir haben jetzt eine träge Datenbank, die uns den Typ jedes Lokals mitteilen kann. Also, zurück zu diesem "Foo". - Wir finden heraus, in welcher Anweisung sich der relevante Ausdruck befindet, und führen dann den semantischen Analysator für genau diese Anweisung aus. Angenommen, Sie haben den Methodenkörper:

String x = "hello";
var y = x.ToCharArray();
var z = from foo in y where foo.

und jetzt müssen wir herausfinden, dass foo vom Typ char ist. Wir erstellen eine Datenbank mit allen Metadaten, Erweiterungsmethoden, Quellcodetypen usw. Wir erstellen eine Datenbank mit Typbestimmungsfaktoren für x, y und z. Wir analysieren die Aussage, die den interessanten Ausdruck enthält. Wir beginnen damit, es syntaktisch zu transformieren

var z = y.Where(foo=>foo.

Um die Art von foo herauszufinden, müssen wir zuerst die Art von y kennen. An dieser Stelle fragen wir den Typbestimmer: "Was ist der Typ von y?" Anschließend wird ein Ausdrucksauswerter gestartet, der x.ToCharArray () analysiert und fragt: "Was ist der Typ von x?" Wir haben einen Typbestimmer für das, der sagt "Ich muss" String "im aktuellen Kontext nachschlagen". Der aktuelle Typ enthält keinen Typ String, daher sehen wir uns den Namespace an. Es ist auch nicht da, also schauen wir in den using-Anweisungen nach und stellen fest, dass es ein "using-System" gibt und dass das System einen Typ-String hat. OK, das ist also die Art von x.

Anschließend fragen wir die Metadaten von System.String nach dem Typ von ToCharArray ab und es wird angegeben, dass es sich um ein System.Char [] handelt. Super. Wir haben also einen Typ für y.

Jetzt fragen wir: "Hat System.Char [] eine Methode, wo?" Also schauen wir uns die using-Direktiven an; Wir haben bereits eine Datenbank vorberechnet, die alle Metadaten für Erweiterungsmethoden enthält, die möglicherweise verwendet werden könnten.

Jetzt sagen wir: "OK, es gibt achtzehn Dutzend Erweiterungsmethoden mit dem Namen Where in scope. Hat eine von ihnen einen ersten formalen Parameter, dessen Typ mit System.Char [] kompatibel ist?" Also starten wir eine Runde Konvertierbarkeitstests. Die Where-Erweiterungsmethoden sind jedoch generisch , was bedeutet, dass wir Typinferenz durchführen müssen.

Ich habe eine spezielle Typ-Infererencing-Engine geschrieben, die unvollständige Inferenzen vom ersten Argument bis zu einer Erweiterungsmethode verarbeiten kann. Wir führen den Typ inferrer aus und stellen fest, dass es eine Where-Methode gibt, die eine nimmt IEnumerable<T>, und dass wir eine Folgerung von System.Char [] nach machen können IEnumerable<System.Char>, also ist T System.Char.

Die Signatur dieser Methode ist Where<T>(this IEnumerable<T> items, Func<T, bool> predicate)und wir wissen, dass T System.Char ist. Wir wissen auch, dass das erste Argument in den Klammern der Erweiterungsmethode ein Lambda ist. Wir starten also einen Inferrer vom Typ Lambda-Ausdruck, der besagt, dass "der formale Parameter foo als System.Char angenommen wird". Verwenden Sie diese Tatsache, wenn Sie den Rest des Lambda analysieren.

Wir haben jetzt alle Informationen, die wir brauchen, um den Körper des Lambda zu analysieren, der "foo" ist. Wir suchen nach der Art des Foo und stellen fest, dass es sich laut Lambda-Binder um System.Char handelt, und wir sind fertig. Wir zeigen Typinformationen für System.Char an.

Und wir machen alles außer der "Top Level" -Analyse zwischen den Tastenanschlägen . Das ist das wirklich Knifflige. Eigentlich ist es nicht schwer, die gesamte Analyse zu schreiben. Es macht es schnell genug, dass Sie es mit einer Schreibgeschwindigkeit tun können, die das wirklich schwierige Stück ist.

Viel Glück!

Eric Lippert
quelle
8
Eric, danke für die vollständige Antwort. Du hast meine Augen ziemlich geöffnet. Für Emacs strebte ich nicht danach, eine dynamische Engine zwischen den Tastenanschlägen zu entwickeln, die in Bezug auf die Qualität der Benutzererfahrung mit Visual Studio konkurrieren würde. Zum einen ist und bleibt die auf Emacs basierende Einrichtung aufgrund der in meinem Design enthaltenen Latenz von ~ 0,5 Sekunden nur auf Anfrage verfügbar . Keine Tippvorschläge. Zum anderen werde ich die grundlegende Unterstützung von Var-Einheimischen implementieren, aber ich werde gerne stechen, wenn die Dinge haarig werden oder wenn der Abhängigkeitsgraph eine bestimmte Grenze überschreitet. Ich bin mir noch nicht sicher, wie hoch diese Grenze ist. Danke noch einmal.
Cheeso
13
Es verwirrt mich ehrlich, dass all dies so schnell und zuverlässig funktionieren kann, insbesondere mit Lambda-Ausdrücken und generischen Typinferenzen. Ich war tatsächlich ziemlich überrascht, als ich zum ersten Mal einen Lambda-Ausdruck schrieb, und Intellisense kannte den Typ meines Parameters, als ich drückte. Obwohl die Anweisung noch nicht vollständig war und ich die generischen Parameter der Erweiterungsmethoden nie explizit angegeben habe. Vielen Dank für diesen kleinen Einblick in die Magie.
Dan Bryant
21
@ Dan: Ich habe den Quellcode gesehen (oder geschrieben) und es verwirrt mich, dass er auch funktioniert. :-) Da sind ein paar haarige Sachen drin.
Eric Lippert
11
Die Eclipse-Jungs machen es wahrscheinlich besser, weil sie großartiger sind als der C # -Compiler und das IDE-Team.
Eric Lippert
23
Ich kann mich überhaupt nicht erinnern, diesen dummen Kommentar abgegeben zu haben. Es macht nicht einmal Sinn. Ich muss betrunken gewesen sein. Es tut uns leid.
Tomas Andrle
15

Ich kann Ihnen ungefähr sagen, wie die Delphi-IDE mit dem Delphi-Compiler zusammenarbeitet, um Intellisense auszuführen (Code Insight nennt Delphi das). Es ist nicht zu 100% auf C # anwendbar, aber es ist ein interessanter Ansatz, der Beachtung verdient.

Die meisten semantischen Analysen in Delphi werden im Parser selbst durchgeführt. Ausdrücke werden beim Parsen eingegeben, außer in Situationen, in denen dies nicht einfach ist. In diesem Fall wird die Vorausschau-Analyse verwendet, um herauszufinden, was beabsichtigt ist, und diese Entscheidung wird dann in der Analyse verwendet.

Bei der Analyse handelt es sich größtenteils um eine rekursive LL (2) -Rückführung, mit Ausnahme von Ausdrücken, die unter Verwendung der Operatorrangfolge analysiert werden. Eines der besonderen Merkmale von Delphi ist, dass es sich um eine Single-Pass-Sprache handelt. Daher müssen Konstrukte deklariert werden, bevor sie verwendet werden. Daher ist kein Top-Level-Pass erforderlich, um diese Informationen herauszubringen.

Diese Kombination von Funktionen bedeutet, dass der Parser ungefähr alle Informationen hat, die für Code Insight an jedem Punkt benötigt werden, an dem sie benötigt werden. So funktioniert es: Die IDE informiert den Lexer des Compilers über die Position des Cursors (den Punkt, an dem Codeeinsicht gewünscht wird), und der Lexer wandelt dies in ein spezielles Token um (es wird als Kibitz-Token bezeichnet). Immer wenn der Parser auf dieses Token trifft (das sich irgendwo befinden könnte), weiß er, dass dies das Signal ist, alle Informationen, die er hat, an den Editor zurückzusenden. Dies geschieht mit einem longjmp, da es in C geschrieben ist. Es benachrichtigt den endgültigen Aufrufer über die Art des syntaktischen Konstrukts (dh den grammatikalischen Kontext), in dem der Kibitz-Punkt gefunden wurde, sowie über alle für diesen Punkt erforderlichen symbolischen Tabellen. So zum Beispiel Wenn sich der Kontext in einem Ausdruck befindet, der ein Argument für eine Methode ist, können wir die Methodenüberladungen überprüfen, die Argumenttypen überprüfen und die gültigen Symbole nur nach denen filtern, die in diesen Argumenttyp aufgelöst werden können (dies reduziert in a viel irrelevante Kruft im Dropdown). Wenn es sich in einem verschachtelten Bereichskontext befindet (z. B. nach einem "."), Hat der Parser einen Verweis auf den Bereich zurückgegeben, und die IDE kann alle in diesem Bereich gefundenen Symbole auflisten.

Andere Dinge werden auch getan; Beispielsweise werden Methodenkörper übersprungen, wenn das Kibitz-Token nicht in ihrer Reichweite liegt. Dies erfolgt optimistisch und wird zurückgesetzt, wenn das Token übersprungen wird. Das Äquivalent zu Erweiterungsmethoden - Klassenhelfer in Delphi - verfügt über eine Art versionierten Cache, sodass die Suche relativ schnell erfolgt. Aber Delphis generische Typinferenz ist viel schwächer als die von C #.

Nun zur spezifischen Frage: Das Ableiten der mit deklarierten Variablentypen varentspricht der Art und Weise, wie Pascal den Konstantentyp ableitet. Es kommt vom Typ des Initialisierungsausdrucks. Diese Typen werden von unten nach oben aufgebaut. Wenn xes vom Typ Integerist und yvom Typ ist Double, dann x + yist es vom Typ Double, weil dies die Regeln der Sprache sind; usw. Sie befolgen diese Regeln, bis Sie auf der rechten Seite einen Typ für den vollständigen Ausdruck haben, und diesen Typ verwenden Sie für das Symbol auf der linken Seite.

Barry Kelly
quelle
7

Wenn Sie keinen eigenen Parser schreiben müssen, um den abstrakten Syntaxbaum zu erstellen, können Sie die Parser von SharpDevelop oder MonoDevelop verwenden , die beide Open Source sind.

Daniel Plaisted
quelle
4

Intellisense-Systeme stellen den Code normalerweise mithilfe eines abstrakten Syntaxbaums dar, mit dem sie den Rückgabetyp der Funktion, die der Variablen 'var' zugewiesen ist, mehr oder weniger auf dieselbe Weise wie der Compiler auflösen können. Wenn Sie VS Intellisense verwenden, stellen Sie möglicherweise fest, dass der Typ var erst angezeigt wird, wenn Sie einen gültigen (auflösbaren) Zuweisungsausdruck eingegeben haben. Wenn der Ausdruck immer noch nicht eindeutig ist (z. B. kann er die generischen Argumente für den Ausdruck nicht vollständig ableiten), wird der var-Typ nicht aufgelöst. Dies kann ein ziemlich komplexer Prozess sein, da Sie möglicherweise ziemlich tief in einen Baum hineingehen müssen, um den Typ aufzulösen. Zum Beispiel:

var items = myList.OfType<Foo>().Select(foo => foo.Bar);

Der Rückgabetyp ist IEnumerable<Bar>, aber um dies zu lösen, ist Folgendes erforderlich:

  1. myList ist vom Typ, der implementiert IEnumerable.
  2. Es gibt eine Erweiterungsmethode, OfType<T>die für IEnumerable gilt.
  3. Der resultierende Wert ist IEnumerable<Foo>und es gibt eine Erweiterungsmethode, Selectdie hierfür gilt.
  4. Der Lambda-Ausdruck foo => foo.Barhat den Parameter foo vom Typ Foo. Dies wird durch die Verwendung von Select abgeleitet, die a benötigt, Func<TIn,TOut>und da TIn bekannt ist (Foo), kann der Typ von foo abgeleitet werden.
  5. Der Typ Foo hat eine Eigenschaftsleiste vom Typ Bar. Wir wissen, dass Select-Rückgaben IEnumerable<TOut>und TOut aus dem Ergebnis des Lambda-Ausdrucks abgeleitet werden können, daher muss der resultierende Elementtyp sein IEnumerable<Bar>.
Dan Bryant
quelle
Richtig, es kann ziemlich tief werden. Ich bin mit dem Auflösen aller Abhängigkeiten zufrieden. Wenn ich nur daran denke, ist die erste von mir beschriebene Option - Kompilieren und Aufrufen - absolut nicht akzeptabel, da das Aufrufen von Code Nebenwirkungen haben kann, wie das Aktualisieren einer Datenbank, und das sollte ein Editor nicht tun. Das Kompilieren ist in Ordnung, das Aufrufen nicht. Was den Aufbau des AST angeht, glaube ich nicht, dass ich das tun möchte. Wirklich, ich möchte diesen Job auf den Compiler verschieben, der bereits weiß, wie es geht. Ich möchte den Compiler bitten können, mir zu sagen, was ich wissen möchte. Ich möchte nur eine einfache Antwort.
Cheeso
Die Herausforderung bei der Überprüfung anhand der Kompilierung besteht darin, dass Abhängigkeiten beliebig tief sein können. Dies bedeutet, dass Sie möglicherweise alles erstellen müssen, damit der Compiler Code generiert. Wenn Sie das tun, können Sie die Debugger-Symbole mit der generierten IL verwenden und den Typ jedes lokalen Symbols damit abgleichen.
Dan Bryant
1
@Cheeso: Der Compiler bietet diese Art der Typanalyse nicht als Service an. Ich hoffe, dass es in Zukunft aber keine Versprechen geben wird.
Eric Lippert
Ja, ich denke, das könnte der richtige Weg sein - alle Abhängigkeiten auflösen und dann IL kompilieren und überprüfen. @ Eric, gut zu wissen. Wenn ich vorerst nicht die vollständige AST-Analyse durchführen möchte, muss ich auf einen Dirty-Hack zurückgreifen, um diesen Service mit vorhandenen Tools zu erstellen. Kompilieren Sie beispielsweise ein intelligent konstruiertes Codefragment und verwenden Sie dann programmgesteuert ILDASM (oder ähnliches), um die gewünschte Antwort zu erhalten.
Cheeso
4

Da Sie auf Emacs abzielen, ist es möglicherweise am besten, mit der CEDET-Suite zu beginnen. Alle Details, die Eric Lippert bereits im Code Analyzer im CEDET / Semantic Tool für C ++ behandelt hat. Es gibt auch einen C # -Parser (der wahrscheinlich ein wenig TLC benötigt), sodass die einzigen fehlenden Teile mit der Optimierung der erforderlichen Teile für C # zusammenhängen.

Die grundlegenden Verhaltensweisen werden in Kernalgorithmen definiert, die von überladbaren Funktionen abhängen, die pro Sprache definiert werden. Der Erfolg der Completion Engine hängt davon ab, wie viel Tuning durchgeführt wurde. Mit C ++ als Leitfaden sollte es nicht schlecht sein, Unterstützung ähnlich wie C ++ zu erhalten.

Daniels Antwort schlägt vor, MonoDevelop zum Parsen und Analysieren zu verwenden. Dies könnte ein alternativer Mechanismus anstelle des vorhandenen C # -Parsers sein oder zum Erweitern des vorhandenen Parsers verwendet werden.

Eric
quelle
Richtig, ich kenne CEDET und verwende die C # -Unterstützung im Contrib-Verzeichnis für Semantik. Semantic bietet die Liste der lokalen Variablen und ihrer Typen. Eine Abschluss-Engine kann diese Liste scannen und dem Benutzer die richtigen Auswahlmöglichkeiten bieten. Das Problem ist, wenn die Variable ist var. Semantic identifiziert es korrekt als var, bietet jedoch keine Typinferenz. Meine Frage war speziell, wie ich das angehen soll . Ich habe auch versucht, mich an die vorhandene CEDET-Fertigstellung anzuschließen, konnte aber nicht herausfinden, wie. Die Dokumentation für CEDET ist ... ah ... nicht vollständig.
Cheeso
Nebenbemerkung - CEDET ist bewundernswert ehrgeizig, aber ich fand es schwierig, es zu verwenden und zu erweitern. Derzeit behandelt der Parser "Namespace" als Klassenindikator in C #. Ich konnte nicht einmal herausfinden, wie man "Namespace" als eigenständiges syntaktisches Element hinzufügt. Dies verhinderte alle anderen syntaktischen Analysen, und ich konnte nicht herausfinden, warum. Ich habe zuvor die Schwierigkeiten erklärt, die ich mit dem Abschlussrahmen hatte. Über diese Probleme hinaus gibt es Nähte und Überlappungen zwischen den Teilen. Zum Beispiel ist die Navigation sowohl Teil der Semantik als auch des Senators. CEDET scheint verlockend, aber am Ende ... ist es zu unhandlich, um sich darauf festzulegen.
Cheeso
Cheeso, wenn Sie die weniger dokumentierten Teile von CEDET optimal nutzen möchten, probieren Sie am besten die Mailingliste aus. Es ist leicht für Fragen, sich mit Bereichen zu befassen, die noch nicht gut entwickelt sind. Daher sind einige Iterationen erforderlich, um gute Lösungen zu erarbeiten oder vorhandene zu erklären. Insbesondere für C # wird es keine einfachen Antworten geben, da ich nichts darüber weiß.
Eric
2

Es ist ein schwieriges Problem, es gut zu machen. Grundsätzlich müssen Sie die Sprachspezifikation / den Compiler durch die meisten Lexing / Parsing / Typechecking modellieren und ein internes Modell des Quellcodes erstellen, das Sie dann abfragen können. Eric beschreibt es ausführlich für C #. Sie können jederzeit den Quellcode des F # -Compilers (Teil des F # CTP) herunterladen und sich service.fsidie Schnittstelle ansehen , die aus dem F # -Compiler verfügbar gemacht wird, den der F # -Sprachendienst für die Bereitstellung von Intellisense, Tooltips für abgeleitete Typen usw. verwendet ein Gefühl für eine mögliche 'Schnittstelle', wenn Sie den Compiler bereits als API zum Aufrufen zur Verfügung hatten.

Die andere Möglichkeit besteht darin, die Compiler unverändert wiederzuverwenden, wie Sie es beschreiben, und dann Reflection zu verwenden oder den generierten Code zu betrachten. Dies ist unter dem Gesichtspunkt problematisch, dass Sie "vollständige Programme" benötigen, um eine Kompilierungsausgabe von einem Compiler zu erhalten, während Sie beim Bearbeiten des Quellcodes im Editor häufig nur "Teilprogramme" haben, die noch nicht analysiert wurden habe alle Methoden noch implementiert, etc.

Kurz gesagt, ich denke, die "Low Budget" -Version ist sehr schwer gut zu machen, und die "echte" Version ist sehr, sehr schwer gut zu machen. (Wo "hart" hier sowohl "Aufwand" als auch "technische Schwierigkeit" misst.)

Brian
quelle
Ja, die "Low Budget" -Version hat einige klare Einschränkungen. Ich versuche zu entscheiden, was "gut genug" ist und ob ich diese Bar treffen kann. Nach meiner eigenen Erfahrung mit Hundefutter, was ich bisher habe, macht es das Schreiben von C # in Emacs viel schöner.
Cheeso
0

Für die Lösung "1" haben Sie eine neue Funktion in .NET 4, um dies schnell und einfach zu erledigen. Wenn Sie also Ihr Programm in .NET 4 konvertieren lassen können, ist dies die beste Wahl.

Softlion
quelle