Pracuję nad narzędziem uzupełniania (Intellisense) dla języka C # w emacsie.
Chodzi o to, że jeśli użytkownik wpisze fragment, a następnie poprosi o uzupełnienie za pomocą określonej kombinacji naciśnięć klawiszy, narzędzie uzupełniania użyje odbicia .NET do określenia możliwych uzupełnień.
Aby to zrobić, trzeba znać rodzaj realizowanej rzeczy. Jeśli jest to ciąg znaków, istnieje znany zestaw możliwych metod i właściwości; jeśli jest to Int32, ma oddzielny zestaw i tak dalej.
Używając semantycznego pakietu leksera / parsera kodu dostępnego w emacsie, mogę zlokalizować deklaracje zmiennych i ich typy. Biorąc to pod uwagę, łatwo jest użyć odbicia, aby uzyskać metody i właściwości typu, a następnie przedstawić listę opcji użytkownikowi. (Ok, nie jest to całkiem proste do zrobienia w emacsie, ale używając możliwości uruchomienia procesu PowerShell wewnątrz emacsa , staje się znacznie łatwiejsze. Piszę niestandardowy zespół .NET, aby wykonać odbicie, załadować go do PowerShell, a następnie elisp uruchomić w nim emacs może wysyłać polecenia do programu PowerShell i odczytywać odpowiedzi przez comint. W rezultacie emacs może szybko uzyskać wyniki refleksji).
Problem pojawia się, gdy kod używa var
w deklaracji rzeczy do wykonania. Oznacza to, że typ nie jest jawnie określony, a uzupełnianie nie będzie działać.
W jaki sposób mogę wiarygodnie określić używany typ, gdy zmienna jest zadeklarowana za pomocą var
słowa kluczowego? Żeby było jasne, nie muszę tego określać w czasie wykonywania. Chcę to określić w „czasie projektowania”.
Do tej pory mam te pomysły:
- kompiluj i wywołuj:
- wyodrębnij deklarację, np. `var foo =" wartość ciągu ";`
- konkatenuje instrukcję `foo.GetType ();`
- dynamicznie skompiluj wynikowy C # fragment go do nowego zestawu
- Załaduj zestaw do nowej domeny AppDomain, uruchom ramkę i pobierz zwracany typ.
- rozładować i wyrzucić zespół
Wiem, jak to wszystko zrobić. Ale brzmi to strasznie ciężko, dla każdego żądania uzupełnienia w edytorze.
Przypuszczam, że nie potrzebuję za każdym razem nowej domeny AppDomain. Mógłbym ponownie użyć pojedynczej domeny AppDomain do wielu tymczasowych zestawów i zamortyzować koszty jego konfiguracji i zerwania w wielu żądaniach ukończenia. To bardziej modyfikacja podstawowej idei.
- skompiluj i sprawdź IL
Po prostu skompiluj deklarację do modułu, a następnie sprawdź IL, aby określić rzeczywisty typ, który został wywnioskowany przez kompilator. Jak byłoby to możliwe? Czego użyłbym do zbadania IL?
Są jakieś lepsze pomysły? Komentarze? propozycje?
EDYCJA - myśląc o tym dalej, kompiluj i wywołuj jest nie do przyjęcia, ponieważ wywołanie może mieć skutki uboczne. Dlatego pierwszą opcję należy wykluczyć.
Myślę też, że nie mogę zakładać obecności .NET 4.0.
AKTUALIZACJA - Prawidłowa odpowiedź, niewymieniona powyżej, ale delikatnie wskazana przez Erica Lipperta, polega na wdrożeniu systemu wnioskowania o pełnej wierności. Jest to jedyny sposób na wiarygodne określenie typu zmiennej w czasie projektowania. Ale to też nie jest łatwe. Ponieważ nie mam złudzeń, że chcę spróbować zbudować coś takiego, wybrałem skrót do opcji 2 - wyodrębnij odpowiedni kod deklaracji i skompiluj go, a następnie sprawdź wynikowy IL.
To faktycznie działa w przypadku sporego podzbioru scenariuszy zakończenia.
Na przykład załóżmy, że w poniższych fragmentach kodu? to pozycja, na której użytkownik prosi o wypełnienie. To działa:
var x = "hello there";
x.?
Ukończenie uświadamia sobie, że x jest ciągiem i zapewnia odpowiednie opcje. Robi to, generując, a następnie kompilując następujący kod źródłowy:
namespace N1 {
static class dmriiann5he { // randomly-generated class name
static void M1 () {
var x = "hello there";
}
}
}
... a następnie inspekcja IL z prostą refleksją.
Działa to również:
var x = new XmlDocument();
x.?
Silnik dodaje odpowiednie klauzule using do wygenerowanego kodu źródłowego tak, aby kompilował się poprawnie, a następnie inspekcja IL jest taka sama.
To też działa:
var x = "hello";
var y = x.ToCharArray();
var z = y.?
Oznacza to po prostu, że inspekcja IL musi znaleźć typ trzeciej zmiennej lokalnej zamiast pierwszej.
I to:
var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var x = z.?
... czyli o jeden poziom głębiej niż w poprzednim przykładzie.
Ale to, co nie działa, to uzupełnianie dowolnej zmiennej lokalnej, której inicjalizacja zależy w dowolnym momencie od elementu członkowskiego instancji lub argumentu metody lokalnej. Lubić:
var foo = this.InstanceMethod();
foo.?
Ani składnia LINQ.
Muszę pomyśleć o tym, jak cenne są te rzeczy, zanim rozważę rozwiązanie ich za pomocą zdecydowanie „ograniczonego projektu” (grzeczne słowo oznaczające hack).
Podejściem do rozwiązania problemu z zależnościami od argumentów metod lub metod instancji byłoby zastąpienie w fragmencie kodu, który jest generowany, kompilowany, a następnie analizowany IL, odniesienia do tych rzeczy „syntetycznymi” lokalnymi zmiennymi tego samego typu.
Kolejna aktualizacja - teraz działa aktualizacja zmiennych zależnych od członków instancji.
To, co zrobiłem, to przesłuchanie typu (za pomocą semantycznej), a następnie wygenerowanie syntetycznych członków zastępczych dla wszystkich istniejących członków. Dla bufora C # takiego jak ten:
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...
... wygenerowany kod, który jest kompilowany, dzięki czemu mogę dowiedzieć się z wyjścia IL typu lokalnego var nnn, wygląda następująco:
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);
}
}
}
Wszystkie elementy członkowskie instancji i typu statycznego są dostępne w kodzie szkieletowym. Kompiluje się pomyślnie. W tym momencie określenie typu zmiennej lokalnej jest proste dzięki odbiciu.
Umożliwia to:
- możliwość uruchomienia programu PowerShell w emacsie
- kompilator C # jest naprawdę szybki. Na moim komputerze kompilacja zestawu w pamięci zajmuje około 0,5 sekundy. Niewystarczająco szybki do analizy między naciśnięciami klawiszy, ale wystarczająco szybki, aby obsługiwać generowanie list ukończenia na żądanie.
Nie zajrzałem jeszcze do LINQ.
Będzie to znacznie większy problem, ponieważ semantyczny lekser / parser, który emacs ma dla C #, nie „robi” LINQ.
źródło
Odpowiedzi:
Mogę ci opisać, jak robimy to efektywnie w "prawdziwym" C # IDE.
Pierwszą rzeczą, jaką robimy, jest uruchomienie przepustki, która analizuje tylko rzeczy „najwyższego poziomu” w kodzie źródłowym. Pomijamy wszystkie treści metody. To pozwala nam szybko zbudować bazę danych zawierającą informacje o przestrzeni nazw, typach i metodach (oraz konstruktorach itp.) W kodzie źródłowym programu. Analizowanie każdego wiersza kodu w każdej treści metody zajęłoby zbyt dużo czasu, jeśli próbujesz to zrobić między naciśnięciami klawiszy.
Kiedy IDE musi wypracować typ konkretnego wyrażenia w treści metody - powiedzmy, że wpisałeś „foo”. i musimy dowiedzieć się, kim są członkowie foo - robimy to samo; pomijamy tak dużo pracy, jak to tylko możliwe.
Zaczynamy od przebiegu, który analizuje tylko deklaracje zmiennych lokalnych w ramach tej metody. Kiedy uruchamiamy ten przebieg, wykonujemy mapowanie z pary „zakresu” i „nazwy” na „wyznacznik typu”. „Określacz typu” to obiekt, który reprezentuje pojęcie „Mogę określić typ tego lokalnego, jeśli zajdzie taka potrzeba”. Wypracowanie typu lokalnego może być kosztowne, więc w razie potrzeby chcemy odłożyć tę pracę.
Mamy teraz leniwie budowaną bazę danych, która może nam powiedzieć typ każdego lokalnego. A więc wracając do tego „foo”. - ustalamy, w której instrukcji znajduje się odpowiednie wyrażenie, a następnie uruchamiamy analizator semantyczny w odniesieniu do tej instrukcji. Na przykład załóżmy, że masz treść metody:
a teraz musimy ustalić, że foo jest typu char. Tworzymy bazę danych, która ma wszystkie metadane, metody rozszerzające, typy kodu źródłowego i tak dalej. Budujemy bazę danych zawierającą wyznaczniki typów dla x, y i z. Analizujemy stwierdzenie zawierające interesujące wyrażenie. Zaczynamy od przekształcenia go składniowo na
Aby obliczyć typ foo, musimy najpierw znać typ y. Więc w tym miejscu pytamy osobę określającą typ „jaki jest typ y”? Następnie uruchamia ewaluator wyrażeń, który analizuje x.ToCharArray () i pyta „jaki jest typ x”? Mamy wyznacznik typu dla tego, który mówi "Muszę wyszukać" Ciąg "w bieżącym kontekście". W bieżącym typie nie ma typu String, więc szukamy w przestrzeni nazw. Tam też go nie ma, więc przeglądamy dyrektywy using i odkrywamy, że istnieje „using System” i że System ma typ String. OK, więc to jest typ x.
Następnie wyszukujemy metadane System.String pod kątem typu ToCharArray i mówi się, że jest to System.Char []. Wspaniały. Mamy więc typ na y.
Teraz pytamy „czy System.Char [] ma metodę Where?” Nie. Więc przyjrzymy się dyrektywom using; już wstępnie obliczyliśmy bazę danych zawierającą wszystkie metadane dla metod rozszerzających, które mogłyby być użyte.
Teraz mówimy "OK, istnieje osiemnaście tuzinów metod rozszerzających o nazwie Gdzie w zakresie, czy którakolwiek z nich ma pierwszy parametr formalny, którego typ jest zgodny z System.Char []?" Rozpoczynamy więc rundę testów konwertowalności. Jednak metody rozszerzające Where są ogólne , co oznacza, że musimy wykonać wnioskowanie o typie.
Napisałem specjalny silnik wnioskujący o typie, który radzi sobie z wyciąganiem niepełnych wniosków z pierwszego argumentu do metody rozszerzającej. Uruchamiamy funkcję wnioskującą o typie i odkrywamy, że istnieje metoda Where, która pobiera an
IEnumerable<T>
, i że możemy wywnioskować z System.Char [] toIEnumerable<System.Char>
, więc T to System.Char.Sygnatura tej metody to
Where<T>(this IEnumerable<T> items, Func<T, bool> predicate)
i wiemy, że T to System.Char. Wiemy również, że pierwszym argumentem wewnątrz nawiasów do metody rozszerzającej jest lambda. Więc zaczynamy wnioskowanie typu wyrażenia lambda, które mówi, że "zakłada się, że parametr formalny foo to System.Char", wykorzystaj ten fakt podczas analizy reszty lambda.Mamy teraz wszystkie informacje potrzebne do przeanalizowania treści lambda, czyli „foo.”. Sprawdzamy typ foo, odkrywamy, że zgodnie z wiązaniem lambda jest to System.Char i gotowe; wyświetlamy informacje o typie dla System.Char.
Robimy wszystko z wyjątkiem analizy „najwyższego poziomu” pomiędzy naciśnięciami klawiszy . To jest naprawdę trudne. Właściwie napisanie całej analizy nie jest trudne; sprawia, że jest na tyle szybki, że można to zrobić z prędkością pisania, co jest naprawdę trudne.
Powodzenia!
źródło
Mogę z grubsza powiedzieć, jak Delphi IDE współpracuje z kompilatorem Delphi w celu wykonania funkcji Intellisense (Delphi nazywa to wglądem w kod). Nie jest w 100% odpowiedni do C #, ale jest to interesujące podejście, które zasługuje na rozważenie.
Większość analiz semantycznych w Delphi jest wykonywana w samym parserze. Wyrażenia są wpisywane podczas ich analizowania, z wyjątkiem sytuacji, w których nie jest to łatwe - w takim przypadku parsowanie antycypacyjne jest używane do ustalenia, co jest zamierzone, a następnie ta decyzja jest używana w analizie.
Analiza jest w dużej mierze rekursywną metodą LL (2), z wyjątkiem wyrażeń, które są analizowane przy użyciu pierwszeństwa operatorów. Jedną z wyróżniających cech Delphi jest to, że jest to język jednoprzebiegowy, więc konstrukcje muszą zostać zadeklarowane przed użyciem, więc nie jest potrzebne żadne przejście najwyższego poziomu, aby wydobyć te informacje.
Ta kombinacja funkcji oznacza, że parser ma z grubsza wszystkie informacje potrzebne do wglądu w kod w dowolnym miejscu, w którym jest to potrzebne. Działa to następująco: IDE informuje leksera kompilatora o pozycji kursora (punkt, w którym wymagany jest wgląd w kod), a lekser zamienia to w specjalny token (nazywa się to tokenem Kibitza). Za każdym razem, gdy parser napotka ten token (który może znajdować się w dowolnym miejscu), wie, że jest to sygnał do odesłania wszystkich posiadanych informacji do redaktora. Robi to używając longjmp, ponieważ jest napisane w C; co robi, to powiadamia ostatecznego rozmówcę o rodzaju konstrukcji składniowej (tj. kontekście gramatycznym), w którym został znaleziony punkt Kibitza, jak również o wszystkich symbolicznych tabelach niezbędnych dla tego punktu. Na przykład jeśli kontekst jest w wyrażeniu, które jest argumentem metody, możemy sprawdzić przeciążenia metody, spojrzeć na typy argumentów i odfiltrować prawidłowe symbole tylko do tych, które mogą rozwiązać ten typ argumentu (to ogranicza wiele nieistotnych skorup w menu rozwijanym). Jeśli jest w kontekście zagnieżdżonego zakresu (np. Po „.”), Parser zwróci odniesienie do zakresu, a IDE może wyliczyć wszystkie symbole znalezione w tym zakresie.
Robione są także inne rzeczy; na przykład, treści metod są pomijane, jeśli token kibitza nie leży w ich zakresie - jest to robione optymistycznie i wycofywane, jeśli przeskakuje token. Odpowiedniki metod rozszerzających - pomocnicy klas w Delphi - mają rodzaj wersjonowanej pamięci podręcznej, więc ich wyszukiwanie jest dość szybkie. Jednak wnioskowanie o typie ogólnym w Delphi jest znacznie słabsze niż w języku C #.
A teraz do konkretnego pytania: wnioskowanie o typach zmiennych zadeklarowanych za pomocą
var
jest równoważne sposobowi, w jaki Pascal wnioskuje o typie stałych. Wynika z typu wyrażenia inicjalizacyjnego. Te typy są budowane od dołu do góry. Jeślix
jest typuInteger
iy
jest typuDouble
, tox + y
będzie typuDouble
, ponieważ takie są reguły języka; itd. Przestrzegasz tych zasad, dopóki nie masz typu pełnego wyrażenia po prawej stronie, a to jest typ, którego używasz dla symbolu po lewej stronie.źródło
Jeśli nie chcesz pisać własnego parsera, aby zbudować abstrakcyjne drzewo składni, możesz przyjrzeć się użyciu parserów z SharpDevelop lub MonoDevelop , z których oba są open source.
źródło
Systemy Intellisense zazwyczaj przedstawiają kod za pomocą abstrakcyjnego drzewa składni, które pozwala im na rozwiązanie typu zwracanego funkcji przypisanej do zmiennej „var” w mniej więcej taki sam sposób, jak zrobi to kompilator. Jeśli używasz VS Intellisense, możesz zauważyć, że nie poda on typu zmiennej, dopóki nie zakończysz wprowadzania prawidłowego (rozpoznawalnego) wyrażenia przypisania. Jeśli wyrażenie jest nadal niejednoznaczne (na przykład nie może w pełni wywnioskować ogólnych argumentów dla wyrażenia), typ var nie zostanie rozstrzygnięty. Może to być dość złożony proces, ponieważ może być konieczne wejście dość głęboko w drzewo, aby rozwiązać typ. Na przykład:
Typ zwracany to
IEnumerable<Bar>
, ale rozwiązanie tego wymaga znajomości:IEnumerable
.OfType<T>
która ma zastosowanie do IEnumerable.IEnumerable<Foo>
i istnieje metoda rozszerzenia,Select
która ma do tego zastosowanie.foo => foo.Bar
ma parametr foo typu Foo. Wynika to z użycia Select, który przyjmuje plikFunc<TIn,TOut>
a ponieważ TIn jest znane (Foo), można wywnioskować typ foo.IEnumerable<TOut>
i TOut można wywnioskować z wyniku wyrażenia lambda, więc wynikowy typ elementów musi byćIEnumerable<Bar>
.źródło
Ponieważ celujesz w Emacsa, najlepiej będzie zacząć od pakietu CEDET. Wszystkie szczegóły, które Eric Lippert są już omówione w analizatorze kodu w narzędziu CEDET / Semantic dla C ++. Istnieje również parser C # (który prawdopodobnie wymaga trochę TLC), więc jedyne brakujące części dotyczą dostrajania niezbędnych części dla C #.
Podstawowe zachowania są zdefiniowane w podstawowych algorytmach, które zależą od funkcji podlegających przeciążeniu, które są zdefiniowane dla każdego języka. Sukces kompletnego silnika zależy od stopnia dostrojenia. Z C ++ jako przewodnikiem, uzyskanie wsparcia podobnego do C ++ nie powinno być takie złe.
Odpowiedź Daniela sugeruje użycie MonoDevelop do parsowania i analizy. Może to być alternatywny mechanizm zamiast istniejącego analizatora składni języka C # lub może zostać użyty do rozszerzenia istniejącego analizatora składni.
źródło
var
. Semantyczny poprawnie identyfikuje go jako zmienną, ale nie zapewnia wnioskowania o typie. Moje pytanie dotyczyło konkretnie tego, jak rozwiązać ten problem . Przyjrzałem się również podłączeniu do istniejącego zakończenia CEDET, ale nie mogłem dowiedzieć się, jak. Dokumentacja CEDET jest ... ach ... niekompletna.Trudno jest zrobić dobrze. Zasadniczo musisz modelować specyfikację języka / kompilator przez większość leksykowania / analizowania / sprawdzania typów i zbudować wewnętrzny model kodu źródłowego, który możesz następnie zapytać. Eric opisuje to szczegółowo dla C #. Zawsze możesz pobrać kod źródłowy kompilatora F # (część F # CTP) i przyjrzeć się
service.fsi
się interfejsowi ujawnionemu z kompilatora F #, który jest używany przez usługę języka F # do zapewniania inteligencji, podpowiedzi dla typów wywnioskowanych itp. poczucie możliwego „interfejsu”, jeśli kompilator był już dostępny jako API do wywołania.Inną drogą jest ponowne użycie kompilatorów w takiej postaci, w jakiej je opisujesz, a następnie użycie odbicia lub przyjrzenie się wygenerowanemu kodowi. Jest to problematyczne z punktu widzenia, że potrzebujesz „pełnych programów”, aby uzyskać dane wyjściowe kompilacji z kompilatora, podczas gdy podczas edytowania kodu źródłowego w edytorze często masz tylko „programy częściowe”, które jeszcze się nie analizują, nie mają wdrożone wszystkie metody itp.
Krótko mówiąc, myślę, że wersja „niskobudżetowa” jest bardzo trudna do wykonania, a wersja „prawdziwa” jest bardzo, bardzo trudna do wykonania. (Gdzie „trudne” oznacza tutaj zarówno „wysiłek”, jak i „trudność techniczną”).
źródło
NRefactory zrobi to za Ciebie.
źródło
W przypadku rozwiązania „1” masz nową funkcję w .NET 4, która umożliwia szybkie i łatwe wykonanie tego. Więc jeśli możesz przekonwertować swój program na .NET 4, byłby to najlepszy wybór.
źródło