Saya sedang mengerjakan fasilitas penyelesaian (intellisense) untuk C # di emacs.
Idenya adalah, jika pengguna mengetikkan sebuah fragmen, kemudian meminta penyelesaian melalui kombinasi penekanan tombol tertentu, fasilitas penyelesaian akan menggunakan refleksi .NET untuk menentukan penyelesaian yang mungkin.
Melakukan ini mengharuskan jenis hal yang diselesaikan, diketahui. Jika itu sebuah string, ada satu set metode dan properti yang diketahui; jika itu Int32, itu memiliki set terpisah, dan seterusnya.
Dengan menggunakan semantik, paket kode lexer / parser yang tersedia di emacs, saya dapat menemukan deklarasi variabel, dan tipenya. Mengingat itu, sangatlah mudah untuk menggunakan refleksi untuk mendapatkan metode dan properti pada tipe, dan kemudian menyajikan daftar opsi kepada pengguna. (Ok, tidak cukup mudah untuk dilakukan dalam emacs, tetapi menggunakan kemampuan untuk menjalankan proses PowerShell di dalam emacs , itu menjadi jauh lebih mudah. Saya menulis rakitan .NET khusus untuk melakukan refleksi, memuatnya ke PowerShell, dan kemudian menjalankan elisp di dalamnya emacs dapat mengirim perintah ke PowerShell dan membaca tanggapan, melalui comint. Hasilnya emacs bisa mendapatkan hasil refleksi dengan cepat.)
Masalahnya muncul ketika kode digunakan var
dalam deklarasi hal yang sedang diselesaikan. Artinya, jenisnya tidak ditentukan secara eksplisit, dan penyelesaian tidak akan berfungsi.
Bagaimana saya bisa dengan andal menentukan tipe sebenarnya yang digunakan, ketika variabel dideklarasikan dengan var
kata kunci? Untuk memperjelas, saya tidak perlu menentukannya saat runtime. Saya ingin menentukannya pada "Waktu desain".
Sejauh ini saya punya ide ini:
- kompilasi dan panggil:
- ekstrak pernyataan deklarasi, misalnya `var foo =" a string value ";`
- menggabungkan pernyataan `foo.GetType ();`
- secara dinamis mengkompilasi fragmen C # yang dihasilkan ke dalam assembly baru
- muat rakitan ke AppDomain baru, jalankan framgment dan dapatkan jenis pengembalian.
- bongkar dan buang rakitan
Saya tahu bagaimana melakukan semua ini. Tapi kedengarannya sangat berat, untuk setiap permintaan penyelesaian di editor.
Saya kira saya tidak membutuhkan AppDomain baru setiap saat. Saya dapat menggunakan kembali AppDomain tunggal untuk beberapa rakitan sementara, dan mengamortisasi biaya pengaturan dan menghancurkannya, di beberapa permintaan penyelesaian. Itu lebih merupakan perubahan dari ide dasarnya.
- mengkompilasi dan memeriksa IL
Cukup kompilasi deklarasi menjadi modul, lalu periksa IL, untuk menentukan tipe sebenarnya yang disimpulkan oleh kompilator. Bagaimana ini mungkin? Apa yang akan saya gunakan untuk memeriksa IL?
Ada ide yang lebih baik di luar sana? Komentar? saran?
EDIT - memikirkan hal ini lebih lanjut, kompilasi-dan-pemanggilan tidak dapat diterima, karena pemanggilan mungkin memiliki efek samping. Jadi, opsi pertama harus dikesampingkan.
Juga, saya rasa saya tidak bisa mengasumsikan adanya .NET 4.0.
PEMBARUAN - Jawaban yang benar, tidak disebutkan di atas, tetapi dengan lembut ditunjukkan oleh Eric Lippert, adalah dengan menerapkan sistem inferensi tipe fidelitas penuh. Ini adalah satu-satunya cara untuk menentukan jenis var pada waktu desain. Tapi, itu juga tidak mudah dilakukan. Karena saya tidak mengalami ilusi bahwa saya ingin mencoba membangun hal seperti itu, saya mengambil jalan pintas dari opsi 2 - mengekstrak kode deklarasi yang relevan, dan mengkompilasinya, kemudian memeriksa IL yang dihasilkan.
Ini benar-benar berfungsi, untuk subset yang adil dari skenario penyelesaian.
Misalnya, dalam fragmen kode berikut,? adalah posisi di mana pengguna meminta penyelesaian. Ini bekerja:
var x = "hello there";
x.?
Penyelesaian menyadari bahwa x adalah String, dan menyediakan opsi yang sesuai. Ini dilakukan dengan membuat dan kemudian menyusun kode sumber berikut:
namespace N1 {
static class dmriiann5he { // randomly-generated class name
static void M1 () {
var x = "hello there";
}
}
}
... dan kemudian memeriksa IL dengan refleksi sederhana.
Ini juga berfungsi:
var x = new XmlDocument();
x.?
Mesin menambahkan klausa penggunaan yang sesuai ke kode sumber yang dihasilkan, sehingga dapat dikompilasi dengan benar, lalu inspeksi IL-nya sama.
Ini juga berfungsi:
var x = "hello";
var y = x.ToCharArray();
var z = y.?
Ini hanya berarti inspeksi IL harus menemukan jenis variabel lokal ketiga, bukan yang pertama.
Dan ini:
var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var x = z.?
... yang hanya satu tingkat lebih dalam dari contoh sebelumnya.
Tapi, yang tidak berhasil adalah penyelesaian pada variabel lokal yang inisialisasinya bergantung pada titik mana pun pada anggota instance, atau argumen metode lokal. Suka:
var foo = this.InstanceMethod();
foo.?
Juga sintaks LINQ.
Saya harus memikirkan betapa berharganya hal-hal itu sebelum saya mempertimbangkan untuk mengatasinya melalui apa yang pasti merupakan "desain terbatas" (kata sopan untuk retasan) untuk penyelesaian.
Pendekatan untuk mengatasi masalah dengan dependensi pada argumen metode atau metode instance adalah dengan mengganti, dalam fragmen kode yang dihasilkan, dikompilasi, dan kemudian dianalisis IL, referensi ke hal-hal tersebut dengan variabel lokal "sintetik" dari jenis yang sama.
Pembaruan Lain - penyelesaian pada vars yang bergantung pada anggota instance, sekarang berfungsi.
Apa yang saya lakukan adalah menginterogasi jenisnya (melalui semantik), dan kemudian menghasilkan anggota siaga sintetis untuk semua anggota yang ada. Untuk buffer C # seperti ini:
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...
... kode yang dihasilkan yang dikompilasi, sehingga saya dapat belajar dari output IL jenis var nnn lokal, terlihat seperti ini:
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);
}
}
}
Semua anggota instance dan tipe statis tersedia dalam kode kerangka. Ini berhasil dikompilasi. Pada titik itu, menentukan jenis var lokal secara langsung melalui Refleksi.
Yang memungkinkan hal ini adalah:
- kemampuan untuk menjalankan PowerShell di emacs
- kompiler C # sangat cepat. Di komputer saya, dibutuhkan sekitar 0,5 detik untuk mengkompilasi rakitan dalam memori. Tidak cukup cepat untuk analisis antar-penekanan tombol, tetapi cukup cepat untuk mendukung pembuatan daftar penyelesaian sesuai permintaan.
Saya belum melihat ke LINQ.
Itu akan menjadi masalah yang jauh lebih besar karena lexer / parser semantik emacs memiliki C #, tidak "melakukan" LINQ.
sumber
Jawaban:
Saya dapat menjelaskan untuk Anda bagaimana kami melakukannya secara efisien dalam C # IDE "nyata".
Hal pertama yang kami lakukan adalah menjalankan pass yang hanya menganalisis hal-hal "tingkat atas" dalam kode sumber. Kami melewatkan semua badan metode. Itu memungkinkan kita untuk dengan cepat membangun database informasi tentang namespace, jenis dan metode (dan konstruktor, dll) yang ada di kode sumber program. Menganalisis setiap baris kode di setiap badan metode akan memakan waktu terlalu lama jika Anda mencoba melakukannya di antara penekanan tombol.
Ketika IDE perlu menentukan jenis ekspresi tertentu di dalam tubuh metode - katakanlah Anda telah mengetik "foo." dan kita perlu mencari tahu apa saja anggota foo - kita melakukan hal yang sama; kita melewatkan pekerjaan sebanyak mungkin.
Kita mulai dengan pass yang hanya menganalisis deklarasi variabel lokal dalam metode itu. Ketika kita menjalankan pass itu kita membuat pemetaan dari sepasang "scope" dan "name" ke "type determiner". "Penentu jenis" adalah objek yang mewakili gagasan "Saya bisa menentukan jenis lokal ini jika saya perlu". Mengerjakan jenis pekerjaan lokal bisa jadi mahal, jadi kami ingin menunda pekerjaan itu jika perlu.
Kami sekarang memiliki database yang dibuat dengan malas yang dapat memberi tahu kami jenis setiap lokal. Jadi, kembali ke "foo" itu. - kita mencari tahu di pernyataan mana ekspresi yang relevan berada dan kemudian menjalankan penganalisis semantik hanya terhadap pernyataan itu. Misalnya, Anda memiliki metode body:
dan sekarang kita perlu mencari tahu bahwa foo adalah tipe char. Kami membangun database yang memiliki semua metadata, metode ekstensi, jenis kode sumber, dan sebagainya. Kami membangun database yang memiliki penentu tipe untuk x, y dan z. Kami menganalisis pernyataan yang berisi ungkapan yang menarik. Kami mulai dengan mengubahnya secara sintaksis menjadi
Untuk mengetahui jenis foo kita harus terlebih dahulu mengetahui jenis y. Jadi pada poin ini kita bertanya kepada penentu tipe "apa tipe y"? Ini kemudian memulai evaluator ekspresi yang mem-parsing x.ToCharArray () dan menanyakan "apa tipe x"? Kami memiliki penentu jenis untuk itu yang mengatakan "Saya perlu mencari" String "dalam konteks saat ini". Tidak ada tipe String di tipe saat ini, jadi kami mencari di namespace. Itu tidak ada di sana juga jadi kita melihat di direktif menggunakan dan menemukan bahwa ada "Sistem menggunakan" dan Sistem itu memiliki tipe String. Oke, jadi itu tipe x.
Kami kemudian menanyakan metadata System.String untuk jenis ToCharArray dan dikatakan bahwa itu adalah System.Char []. Super. Jadi kami memiliki tipe untuk y.
Sekarang kita bertanya "apakah System.Char [] memiliki metode Dimana?" Tidak. Jadi kita melihat menggunakan arahan; kami telah menghitung database yang berisi semua metadata untuk metode ekstensi yang mungkin dapat digunakan.
Sekarang kita berkata "Oke, ada delapan belas lusin metode ekstensi bernama Where in scope, apakah salah satu dari mereka memiliki parameter formal pertama yang tipenya kompatibel dengan System.Char []?" Jadi kami memulai putaran pengujian konvertibilitas. Namun, metode ekstensi Where adalah generik , yang berarti kita harus melakukan inferensi tipe.
Saya telah menulis mesin infererencing tipe khusus yang dapat menangani pembuatan kesimpulan yang tidak lengkap dari argumen pertama ke metode ekstensi. Kami menjalankan tipe inferrer dan menemukan bahwa ada metode Di mana yang mengambil
IEnumerable<T>
, dan kami dapat membuat kesimpulan dari System.Char [] keIEnumerable<System.Char>
, jadi T adalah System.Char.Tanda tangan dari metode ini adalah
Where<T>(this IEnumerable<T> items, Func<T, bool> predicate)
, dan kita tahu bahwa T adalah System.Char. Kita juga tahu bahwa argumen pertama di dalam tanda kurung untuk metode ekstensi adalah lambda. Jadi kita memulai inferrer jenis ekspresi lambda yang mengatakan "parameter formal foo diasumsikan System.Char", gunakan fakta ini saat menganalisis lambda lainnya.Kami sekarang memiliki semua informasi yang kami butuhkan untuk menganalisis tubuh lambda, yaitu "foo.". Kami mencari jenis foo, kami menemukan bahwa menurut pengikat lambda itu adalah System.Char, dan kami selesai; kami menampilkan informasi jenis untuk System.Char.
Dan kami melakukan segalanya kecuali analisis "tingkat atas" di antara penekanan tombol . Itu sedikit rumit. Sebenarnya menulis semua analisis tidaklah sulit; itu membuatnya cukup cepat sehingga Anda dapat melakukannya dengan kecepatan mengetik yang benar-benar rumit.
Semoga berhasil!
sumber
Saya dapat memberitahu Anda secara kasar bagaimana Delphi IDE bekerja dengan kompiler Delphi untuk melakukan intellisense (kode wawasan adalah apa yang Delphi sebut itu). Ini tidak 100% berlaku untuk C #, tapi ini adalah pendekatan menarik yang patut dipertimbangkan.
Sebagian besar analisis semantik di Delphi dilakukan di parser itu sendiri. Ekspresi diketik saat diurai, kecuali untuk situasi di mana hal ini tidak mudah - dalam hal ini penguraian tampilan ke depan digunakan untuk mengetahui apa yang diinginkan, dan kemudian keputusan tersebut digunakan dalam penguraian.
Parse sebagian besar adalah LL (2) turunan rekursif, kecuali untuk ekspresi, yang diurai menggunakan prioritas operator. Salah satu hal yang berbeda tentang Delphi adalah bahwa ini adalah bahasa single-pass, jadi konstruksi perlu dideklarasikan sebelum digunakan, jadi tidak diperlukan pass level atas untuk mengeluarkan informasi itu.
Kombinasi fitur ini berarti bahwa pengurai memiliki kira-kira semua informasi yang diperlukan untuk wawasan kode untuk setiap titik yang membutuhkannya. Cara kerjanya adalah sebagai berikut: IDE menginformasikan lexer kompiler tentang posisi kursor (titik di mana wawasan kode diinginkan) dan lexer mengubahnya menjadi token khusus (disebut token kibitz). Setiap kali parser bertemu dengan token ini (yang bisa berada di mana saja) ia tahu bahwa ini adalah sinyal untuk mengirim kembali semua informasi yang dimilikinya kembali ke editor. Ia melakukan ini menggunakan longjmp karena ditulis dalam C; apa yang dilakukannya adalah memberi tahu pemanggil terakhir tentang jenis konstruksi sintaksis (yaitu konteks tata bahasa) tempat titik kibitz ditemukan, serta semua tabel simbolik yang diperlukan untuk titik itu. Misalnya, jika konteksnya berada dalam ekspresi yang merupakan argumen untuk suatu metode, kita dapat memeriksa kelebihan metode, melihat tipe argumen, dan memfilter simbol yang valid hanya untuk yang dapat menyelesaikan tipe argumen itu (ini memotong dalam banyak cruft yang tidak relevan di drop-down). Jika berada dalam konteks lingkup bersarang (misalnya setelah "."), Parser akan mengembalikan referensi ke lingkup, dan IDE dapat menyebutkan semua simbol yang ditemukan dalam lingkup itu.
Hal-hal lain juga dilakukan; misalnya, badan metode dilewati jika token kibitz tidak berada dalam jangkauannya - ini dilakukan secara optimis, dan dibatalkan jika token dilewati. Setara dengan metode ekstensi - pembantu kelas di Delphi - memiliki semacam cache berversi, sehingga pencariannya cukup cepat. Tapi tipe inferensi umum Delphi jauh lebih lemah daripada C #.
Sekarang, ke pertanyaan khusus: menyimpulkan jenis variabel yang dideklarasikan dengan
var
setara dengan cara Pascal menyimpulkan jenis konstanta. Itu berasal dari jenis ekspresi inisialisasi. Jenis ini dibangun dari bawah ke atas. Ifx
is of typeInteger
, andy
is of typeDouble
, makax + y
akan menjadi typeDouble
, karena itulah aturan bahasanya; dll. Anda mengikuti aturan ini sampai Anda memiliki tipe untuk ekspresi penuh di sisi kanan, dan itu adalah tipe yang Anda gunakan untuk simbol di sebelah kiri.sumber
Jika Anda tidak ingin menulis parser Anda sendiri untuk membangun pohon sintaksis abstrak, Anda dapat menggunakan parser dari SharpDevelop atau MonoDevelop , keduanya open source.
sumber
Sistem Intellisense biasanya merepresentasikan kode menggunakan Pohon Sintaks Abstrak, yang memungkinkan mereka untuk menyelesaikan jenis kembalian dari fungsi yang ditugaskan ke variabel 'var' dengan cara yang kurang lebih sama seperti yang akan dilakukan kompilator. Jika Anda menggunakan VS Intellisense, Anda mungkin memperhatikan bahwa itu tidak akan memberi Anda tipe var sampai Anda selesai memasukkan ekspresi penugasan yang valid (dapat diatasi). Jika ekspresi masih ambigu (misalnya, tidak dapat sepenuhnya menyimpulkan argumen umum untuk ekspresi tersebut), jenis var tidak akan menyelesaikan. Ini bisa menjadi proses yang cukup rumit, karena Anda mungkin perlu berjalan cukup jauh ke dalam pohon untuk menyelesaikan jenisnya. Misalnya:
Jenis kembaliannya adalah
IEnumerable<Bar>
, tetapi menyelesaikan ini membutuhkan pengetahuan:IEnumerable
.OfType<T>
yang berlaku untuk IEnumerable.IEnumerable<Foo>
dan ada metode ekstensiSelect
yang berlaku untuk ini.foo => foo.Bar
memiliki parameter foo berjenis Foo. Hal ini disimpulkan dengan penggunaan Select, yang membutuhkanFunc<TIn,TOut>
dan karena TIn diketahui (Foo), jenis foo dapat disimpulkan.IEnumerable<TOut>
dan TOut dapat disimpulkan dari hasil ekspresi lambda, jadi jenis item yang dihasilkan haruslahIEnumerable<Bar>
.sumber
Karena Anda menargetkan Emacs, mungkin yang terbaik adalah memulai dengan rangkaian CEDET. Semua detail yang Eric Lippert tercakup dalam penganalisis kode di alat CEDET / Semantic untuk C ++ sudah. Ada juga parser C # (yang mungkin membutuhkan sedikit TLC) sehingga satu-satunya bagian yang hilang terkait dengan penyetelan bagian yang diperlukan untuk C #.
Perilaku dasar ditentukan dalam algoritme inti yang bergantung pada fungsi yang dapat diisi berlebih yang ditentukan per bahasa. Keberhasilan mesin penyelesaian tergantung pada seberapa banyak penyetelan yang telah dilakukan. Dengan c ++ sebagai panduan, mendapatkan dukungan yang mirip dengan C ++ seharusnya tidak terlalu buruk.
Jawaban Daniel menyarankan penggunaan MonoDevelop untuk melakukan parsing dan analisis. Ini bisa menjadi mekanisme alternatif daripada pengurai C # yang sudah ada, atau bisa digunakan untuk menambah pengurai yang ada.
sumber
var
. Semantic dengan benar mengidentifikasinya sebagai var, tetapi tidak memberikan inferensi tipe. Pertanyaan saya adalah khusus sekitar bagaimana untuk mengatasi itu . Saya juga mencari cara untuk memasukkan penyelesaian CEDET yang ada, tetapi saya tidak tahu caranya. Dokumentasi untuk CEDET adalah ... ah ... tidak lengkap.Sulit untuk melakukannya dengan baik. Pada dasarnya Anda perlu memodelkan spek / kompiler bahasa melalui sebagian besar pemeriksaan lexing / parsing / typecheck dan membangun model internal kode sumber yang kemudian dapat Anda kueri. Eric menjelaskannya secara rinci untuk C #. Anda selalu dapat mengunduh kode sumber kompilator F # (bagian dari F # CTP) dan
service.fsi
melihat antarmuka yang diekspos keluar dari kompilator F # yang digunakan layanan bahasa F # untuk menyediakan intellisense, keterangan alat untuk tipe yang disimpulkan, dll. rasa 'antarmuka' yang mungkin jika Anda sudah memiliki kompiler yang tersedia sebagai API untuk dipanggil.Rute lainnya adalah dengan menggunakan kembali kompiler apa adanya seperti yang Anda gambarkan, dan kemudian gunakan refleksi atau lihat kode yang dihasilkan. Ini bermasalah dari sudut pandang bahwa Anda memerlukan 'program lengkap' untuk mendapatkan output kompilasi dari kompiler, sedangkan saat mengedit kode sumber di editor, Anda seringkali hanya memiliki 'program parsial' yang belum diurai, jangan sudah menerapkan semua metode, dll.
Singkatnya, menurut saya versi 'anggaran rendah' sangat sulit dilakukan dengan baik, dan versi 'nyata' sangat, sangat sulit dilakukan dengan baik. (Di mana 'keras' di sini mengukur 'upaya' dan 'kesulitan teknis'.)
sumber
NRefactory akan melakukan ini untuk Anda.
sumber
Untuk solusi "1" Anda memiliki fasilitas baru di .NET 4 untuk melakukan ini dengan cepat dan mudah. Jadi, jika Anda dapat mengubah program Anda menjadi .NET 4, itu akan menjadi pilihan terbaik Anda.
sumber