DSL Universal. Apakah ini mungkin?


Bahasa bidang subjek. Tidak kelebihan dengan konstruksi bahasa tujuan umum. Pada saat yang sama, ini memungkinkan Anda untuk menerapkan logika yang sangat kompleks hanya dengan beberapa baris. Semua ini adalah DSL.

Namun, pembuatan DSL mengharuskan pengembang untuk memiliki kualifikasi. Penggunaan pendekatan ini secara rutin berubah menjadi rutinitas mengembangkan bahasa lain. Solusinya mungkin dengan membuat alat universal - mesin yang akan berlaku untuk tugas yang sama sekali berbeda dan mudah dimodifikasi. Pada artikel ini, kami akan mengembangkan dalam bahasa C # yang paling sederhana dari sudut pandang implementasi, tetapi pada saat yang sama mesin bahasa yang cukup kuat, dengan mana Anda dapat memecahkan berbagai masalah yang cukup luas.

Pendahuluan


Ada dua cara untuk mengembangkan proyek aplikasi: untuk membuatnya sangat sederhana sehingga jelas bahwa ia tidak memiliki kekurangan, atau membuatnya sangat kompleks sehingga tidak memiliki kekurangan yang jelas. C.E. R. Hoar (CAR Hoare)
Dalam artikel ini saya ingin berbagi salah satu teknik pengembangan yang membantu saya dan tim saya, di satu sisi, menangani kompleksitas proyek. Dan di sisi lain - ini memungkinkan Anda untuk dengan cepat mengembangkan aplikasi prototipe. Pada pandangan pertama, mengembangkan bahasa pemrograman tampaknya terlalu rumit. Jadi, jika kita berbicara tentang alat universal. Jika tujuannya adalah untuk mencakup bidang subjek yang sempit, maka pengembangan bahasa tertentu sering membenarkan dirinya sendiri.

Suatu kali saya dihadapkan dengan tugas mengembangkan implementasi bahasa industri (IEC 61131-3) untuk ditanamkan dalam perangkat lunak pelanggan. Dalam perjalanan pekerjaan ini, saya menjadi tertarik dengan topik struktur juru bahasa dan sejak itu saya telah menulis penerjemah esoterik dan bukan bahasa sebagai hobi. Di masa depan, pemahaman datang tentang bagaimana menggunakan penerjemah yang ditulis sendiri untuk menyederhanakan kehidupan sehari-hari.


Tujuan utama dari bahasa pemrograman yang waras adalah menyederhanakan proses pemrograman dan membaca suatu program. Menulis dalam asm lebih mudah daripada dalam kode mesin, menulis dalam C lebih mudah daripada dalam asm, dalam C # bahkan lebih sederhana dan seterusnya.

Ini dicapai terutama karena metode reduksionisme yang paling populer - membagi tugas yang kompleks menjadi komponen yang sederhana dan mudah dikenali - menstandarisasi interaksi mereka dan sintaksis tertentu.

Bahasa pemrograman terdiri dari seperangkat operator, yang pada dasarnya adalah dasar dari bahasa, blok bangunan dasar, dan sintaksis yang mendefinisikan cara penulisan kombinasi operator, serta perpustakaan standar. Urutan tindakan dasar menurut aturan sintaksis dikelompokkan ke dalam fungsi, fungsi dikelompokkan ke dalam kelas (jika ada OOP), kelas digabungkan ke dalam perpustakaan, dan mereka, pada gilirannya, menjadi paket. Ini adalah apa yang terlihat seperti bahasa arus utama. Pada prinsipnya, teknik ini cukup untuk menyelesaikan sebagian besar tugas sehari-hari. Namun, ini bukan batasnya, karena Anda dapat mengambil langkah lebih jauh - ke tingkat abstraksi yang lebih tinggi, dan Anda harus melampaui batas bahasa yang digunakan jika tidak mendukung metaprogramming dalam bentuk makro.


Saat ini, sebagian besar proyek bermuara pada kombinasi komponen siap pakai dan bagian tulisan rendah tingkat rendah yang tidak signifikan. Kombinasi komponen biasanya dilakukan dengan menggunakan bahasa pemrograman universal - C #, Java, Python, dan lainnya. Meskipun bahasa-bahasa ini adalah tingkat tinggi, mereka juga universal, dan karena itu mengandung konstruksi sintaksis untuk operasi tingkat rendah, pembuatan fungsi, kelas, deskripsi jenis umum, pemrograman asinkron, dan banyak lagi. Karena itu, tugas "Lakukan sekali, lakukan dua, lakukan tiga" ditumbuhi dengan banyak konstruksi sintaksis dan dapat membengkak hingga ratusan baris kode dan banyak lagi.

Anda dapat menyederhanakan penggunaan kembali komponen jika Anda mengulangi teknik reduksionisme, tetapi sudah pada komponen-komponen ini. Ini dicapai melalui pengembangan bahasa khusus yang memiliki sintaks yang disederhanakan dan berfungsi semata-mata untuk menggambarkan interaksi komponen-komponen ini. Pendekatan ini disebut YaOP (pemrograman berorientasi bahasa), dan bahasa disebut DSL (Domain-Specific Language - bahasa khusus domain).

Karena kurangnya konstruksi yang berlebihan, hanya beberapa baris pada DSL yang dapat mengimplementasikan fungsionalitas yang agak rumit, yang mengarah pada konsekuensi positif: kecepatan pengembangan meningkat, jumlah kesalahan berkurang, dan pengujian sistem disederhanakan.

Jika diterapkan dengan sukses, pendekatan ini dapat secara signifikan meningkatkan fleksibilitas produk yang sedang dikembangkan karena kemungkinan penulisan skrip kompak yang mendefinisikan dan memperluas perilaku sistem. Mungkin ada banyak aplikasi untuk pendekatan ini, sebagaimana dibuktikan oleh prevalensi pendekatan ini, karena DSL ada di mana-mana. HTML umum adalah bahasa deskripsi dokumen, SQL adalah bahasa query terstruktur, JSON adalah bahasa deskripsi data terstruktur, XAML, PostScript, Emacs Lisp, nnCron dan banyak lainnya.


Dengan semua kelebihannya, DSL memiliki kelemahan signifikan - persyaratan tinggi untuk pengembang sistem.

Tidak semua pengembang memiliki pengetahuan dan pengalaman dalam mengembangkan bahasa primitif sekalipun. Bahkan sejumlah kecil spesialis dapat mengembangkan bahasa yang cukup fleksibel dan produktif. Ada masalah lain. Misalnya, pada titik tertentu dalam pengembangan fungsionalitas yang awalnya ditetapkan, itu mungkin tidak cukup dan akan diperlukan untuk membuat fungsi atau OOP. Dan di mana ada fungsi, optimisasi rekursi ekor mungkin diperlukan untuk dilakukan tanpa loop, dan sebagainya. Pada saat yang sama, kompatibilitas ke belakang harus diperhitungkan sehingga skrip yang ditulis sebelumnya terus bekerja dengan versi baru.

Masalah lain adalah bahwa bahasa yang dirancang untuk memecahkan satu masalah sama sekali tidak cocok untuk orang lain. Karena itu, Anda harus mengembangkan DSL baru dari awal, sehingga pengembangan bahasa baru menjadi rutin. Ini lagi mempersulit pemeliharaan dan mengurangi penggunaan kembali kode yang sulit untuk dibagikan antara implementasi DSL yang berbeda dan proyek yang menggunakannya.


Jalan keluarnya adalah membuat DSL untuk membangun DSL. Di sini saya tidak bermaksud RBNF, melainkan sebuah bahasa yang dapat diubah dengan built-in berarti ke bahasa area subjek. Kendala utama dalam menciptakan bahasa yang fleksibel dan dapat ditransformasikan adalah keberadaan sistem sintaks dan tipe yang didefinisikan dengan kaku. Selama seluruh periode pengembangan industri komputer, beberapa bahasa fleksibel tanpa sintaks telah diusulkan, tetapi mereka bertahan sampai hari ini dan bahasa Forth dan Lisp terus berkembang secara aktif. Fitur utama dari bahasa-bahasa ini adalah bahwa, karena struktur dan ikon-homo mereka, mereka dapat, karena alat bawaan, mengubah perilaku penerjemah dan, jika perlu, konstruksi sintaksis parse yang tidak diletakkan pada awalnya.

Ada solusi untuk Forth memperluas sintaksnya ke C atau ke Skema. "Fort" sering dikritik karena urutan postfix yang tidak biasa dari argumen dan operasi, yang ditentukan oleh penggunaan stack untuk meneruskan argumen. Namun, "Fort" memiliki akses ke penerjemah teks, ini memungkinkan Anda untuk menyembunyikan catatan balik dari pengguna jika perlu. Dan, akhirnya, ini masalah kebiasaan, dan ini dikembangkan dengan cukup cepat.

Keluarga bahasa Lisp bergantung pada makro yang memungkinkan Anda untuk masuk DSL jika perlu. Dan akses ke penerjemah dan pembaca berkontribusi pada penerapan penafsir metaklikus dengan fitur penafsiran tertentu. Sebagai contoh, implementasi Scheme lisp Racket diposisikan sebagai lingkungan untuk mengembangkan bahasa dan memiliki bahasa out of the box untuk membuat server web, membangun antarmuka GUI, bahasa inferensi, dan lainnya.

Fleksibilitas semacam itu menjadikan bahasa ini kandidat yang baik untuk peran mesin DSL universal.

β€œFort” dan Lisp sebagian besar berkembang sebagai bahasa tujuan umum, meskipun niche, sebagai hasilnya - mereka menggunakan fungsionalitas yang dapat digunakan untuk bahasa DSL. Tetapi pada saat yang sama mereka cukup sederhana untuk diimplementasikan, yang berarti Anda dapat mengembangkan versi terbatas dengan kemungkinan perluasannya. Ini akan memungkinkan Anda untuk menggunakan kembali inti dari bahasa tersebut dengan modifikasi kecil (idealnya - tanpa) untuk tugas tertentu.

Saya juga ingin mencatat bahwa bahasa-bahasa ini hebat tidak hanya untuk menulis skrip, tetapi juga untuk interaksi interaktif dengan sistem melalui REPL. Yang, di satu sisi, bisa nyaman untuk debugging, dan di sisi lain, bertindak sebagai antarmuka yang dapat diakses pengguna dengan sistem. Diyakini bahwa antarmuka teks dengan sistem dalam beberapa kasus dapat lebih efektif daripada yang grafis, karena jauh lebih sederhana untuk diimplementasikan, lebih fleksibel, memungkinkan pengguna untuk menggeneralisasi operasi tipikal menjadi fungsi, dan sebagainya. Contoh mencolok dari antarmuka teks mungkin Bash. Dan jika bahasanya homo-ikon, maka konstruksinya relatif mudah untuk dibuat dan diurai dan dengan sedikit upaya mengimplementasikan bahasa grafis di atas penerjemah - ini bisa berguna ketika pengguna target jauh dari pemrograman.

Saat ini, bahasa deskripsi data XML dan JSON banyak digunakan sebagai DSL untuk konfigurasi. Tentu saja, ini adalah praktik yang hebat, tetapi dalam beberapa kasus data saja tidak cukup dan Anda perlu, misalnya, untuk menggambarkan operasi pada mereka.


Dalam posting ini, saya mengusulkan untuk membuat penerjemah sederhana bahasa Fort dan menunjukkan bagaimana mengadaptasinya untuk menyelesaikan masalah tertentu.

Bahasa Fort dipilih sebagai yang paling mudah untuk diimplementasikan dan digunakan, sementara cukup kuat untuk menggunakannya sebagai DSL untuk sejumlah tugas. Faktanya, inti dari bahasa adalah penerjemah alamat, yang bahkan dalam assembler hanya mengambil beberapa baris, dan sebagian besar implementasinya jatuh pada primitif, yang lebih, lebih universal, cepat dan fleksibel pelaksanaannya. Bagian penting lain dari bahasa ini adalah penerjemah teks, yang memungkinkan Anda berinteraksi dengan penerjemah alamat.


Penerjemah alamat


Elemen dasar dari bahasa Fort adalah kata yang dipisahkan dari kata dan atom lainnya (angka) dengan spasi, ujung garis dan tab.

Sebuah kata memiliki arti dan sifat yang sama dengan fungsi dari bahasa lain, misalnya C. Kata-kata yang terhubung dalam implementasi, yaitu, diimplementasikan dengan cara yang sama dengan penerjemah, serupa dengan operator dari bahasa lain. Sebenarnya, sebuah program dalam bahasa pemrograman apa pun tidak lebih dari kombinasi bahasa dan operator data. Oleh karena itu, pembuatan bahasa pemrograman dapat dianggap sebagai definisi operator dan cara menggabungkannya. Selain itu, bahasa seperti C menentukan cara penulisan operator yang berbeda, yang menentukan sintaksis bahasa tersebut. Di sebagian besar bahasa, memodifikasi pernyataan biasanya tidak mungkin - misalnya, Anda tidak dapat mengubah sintaks atau perilaku pernyataan if.

Dalam bahasa Fort, semua operator dan kombinasinya (kata-kata pengguna) memiliki metode penulisan yang sama. Kata-kata benteng dibagi menjadi primitif dan adat. Anda dapat mendefinisikan kata yang akan membebani primitif, sehingga mengubah perilaku primitif. Meskipun dalam kenyataannya kata yang didefinisikan ulang akan diimplementasikan melalui primitif yang awalnya ditentukan. Dalam implementasi kami, fungsi dalam C # akan menjadi primitif. Kata yang ditentukan pengguna terdiri dari daftar alamat kata yang akan dieksekusi. Karena ada dua jenis kata, penafsir harus membedakannya. Pemisahan kata primitif dan kata pengguna dilakukan melalui kata primitif yang sama, setiap kata pengguna dimulai dengan operasi DoList dan berakhir dengan operasi Keluar.

Dimungkinkan untuk menggambarkan untuk waktu yang lama bagaimana pemisahan seperti itu terjadi, tetapi lebih mudah untuk memahami ini dengan mempelajari urutan pelaksanaan program juru bahasa. Untuk melakukan ini, kami menerapkan juru bahasa minimal, mendefinisikan program sederhana dan melihat bagaimana itu akan dilakukan langkah demi langkah.


Mesin benteng kami terdiri dari memori linier, tumpukan data, tumpukan kembali, penunjuk instruksi, penunjuk kata. Kami juga akan memiliki tempat terpisah untuk menyimpan primitif.

public object[] Mem; //   public Stack<int> RS; //   public Stack<object> DS; //   public int IP; //   public int WP; //   public delegate void CoreCall(); public List<CoreCall> Core; //   

Inti dari interpretasi adalah untuk menavigasi ke alamat dalam memori dan untuk menjalankan instruksi yang ditunjukkan di sana. Seluruh penerjemah alamat - inti bahasa - dalam kasus kami akan ditentukan dalam satu fungsi Berikutnya ().

 public void Next() { while (true) { if (IP == 0) return; WP = (int)Mem[IP++]; Core[(int)Mem[WP]](); } } 

Setiap kata pengguna dimulai dengan perintah DoList, yang tugasnya adalah menyimpan alamat interpretasi saat ini di stack dan mengatur alamat interpretasi dari kata berikutnya.

 public void DoList() { RS.Push(IP); IP = WP + 1; } 

Untuk keluar dari kata, gunakan perintah Keluar, yang mengembalikan alamat dari tumpukan kembali.

 public void Exit() { IP = RS.Pop(); } 

Untuk demonstrasi visual dari prinsip interpreter, kami memperkenalkan sebuah perintah, itu akan mensimulasikan pekerjaan yang bermanfaat. Sebut saja Halo ().

 public void Hello() { Console.WriteLine("Hello"); } 

Pertama, Anda perlu menginisialisasi mesin dan menentukan primitif untuk penerjemah agar bekerja dengan benar. Anda juga perlu menentukan alamat primitif dalam memori program.

 Mem = new Object[1024]; RS = new Stack<int>(); DS = new Stack<object>(); Core = new List<CoreCall>(); Core.Add(Next); Core.Add(DoList); Core.Add(Exit); Core.Add(Hello); const int opNext = 0; const int opDoList = 1; const int opExit = 2; const int opHello = 3; // core pointers Mem[opNext] = opNext; Mem[opDoList] = opDoList; Mem[opExit] = opExit; Mem[opHello] = opHello; 

Sekarang kita dapat membuat program sederhana, dalam kasus kami, kode pengguna akan mulai dari alamat 4 dan terdiri dari dua subprogram. Rutin pertama dimulai pada alamat 7 dan memanggil yang kedua, yang dimulai pada alamat 4 dan menampilkan kata Halo.

 // program Mem[4] = opDoList; // 3)    IP = 9   ,   IP = WP + 1 = 5 Mem[5] = opHello; // 4)     Mem[6] = opExit; // 5)   ,  IP = 9    Mem[7] = opDoList; // 1)     Mem[8] = 4; // 2)     4,  WP = 4 Mem[9] = opExit; // 6)   ,  IP = 0    

Untuk menjalankan program, Anda harus terlebih dahulu menyimpan nilai 0 pada tumpukan kembali, di mana penerjemah alamat akan menginterupsi siklus interpretasi, dan mengatur titik masuk, dan kemudian memulai penerjemah.

 var entryPoint = 7; //    IP = 0; //  IP = 0,        WP = entryPoint; //  WP = 7      DoList(); //     ,  IP = 0    Next(); //    

Seperti yang dijelaskan, pada primitif interpreter ini akan disimpan dalam memori terpisah. Tentu saja, itu bisa diimplementasikan secara berbeda: misalnya, dalam memori program, delegasi ke fungsi operator disimpan. Di satu sisi, juru bahasa seperti itu tidak akan menjadi lebih mudah, tetapi di sisi lain, itu akan jelas lebih lambat, karena setiap langkah interpretasi akan memerlukan pengecekan jenis, casting dan eksekusi, lebih banyak operasi diperoleh.

Setiap kata pengguna dari juru bahasa kami dimulai dengan primitif DoList, yang tugasnya adalah menyimpan alamat interpretasi saat ini dan pergi ke alamat berikutnya. Keluar dari subrutin dilakukan oleh operasi Keluar, yang mengembalikan alamat dari tumpukan kembali untuk interpretasi lebih lanjut. Faktanya, kami telah menggambarkan seluruh penerjemah alamat. Untuk menjalankan program arbitrer, cukup mengembangkannya dengan primitif. Tetapi pertama-tama Anda perlu berurusan dengan penerjemah teks, yang menyediakan antarmuka ke penerjemah alamat.


Penerjemah teks


Bahasa Fort tidak memiliki sintaksis, program yang ditulis di dalamnya adalah kata-kata yang dipisahkan oleh spasi, tab, atau akhir baris. Oleh karena itu, tugas penerjemah teks adalah memecah aliran input menjadi kata-kata (token), menemukan titik masuknya, mengeksekusi atau menulis ke memori. Tetapi tidak semua token dapat dieksekusi. Jika penerjemah tidak menemukan kata, ia mencoba menafsirkannya sebagai konstanta numerik. Selain itu, penerjemah teks memiliki dua mode: mode interpretasi dan mode pemrograman. Dalam mode pemrograman, alamat kata tidak dieksekusi, tetapi ditulis ke dalam memori, sehingga kata-kata baru ditentukan.

Implementasi kanonik dari "Benteng" biasanya menggabungkan kamus (entri kamus) dan memori program, mendefinisikan file kode tunggal dalam bentuk daftar yang hanya terhubung. Dalam implementasi kami, hanya kode yang dapat dieksekusi yang akan ada di memori, dan titik masuk kata-kata akan disimpan dalam struktur terpisah - kamus.

 public Dictionary<string, List<WordHeader>> Entries; 

Dalam kamus ini, kata tersebut diberikan ke beberapa judul, sehingga Anda dapat menentukan jumlah subprogram sewenang-wenang dengan nama yang sama, lalu menghapus definisi ini dan mulai menggunakan yang lama. Selain itu, alamat lama yang disimpan memungkinkan Anda untuk menemukan nama kata dalam kamus, bahkan jika telah didefinisikan ulang, yang sangat berguna untuk menghasilkan jejak tumpukan atau untuk debugging untuk mempelajari memori. WordHeader adalah kelas yang menyimpan alamat entri subrutin dan bendera interpretasi langsung.

 public class WordHeader { public int Address; public bool Immediate; } 

Bendera langsung memerintahkan juru bahasa bahwa kata ini harus dijalankan dalam mode pemrograman, dan tidak ditulis ke memori. Secara skematis, logika penafsir dapat direpresentasikan sebagai berikut: tangan kanan adalah YA, kiri adalah TIDAK.


Kami akan menggunakan TextReader untuk membaca aliran input, dan TextWriter untuk menampilkannya.

 public TextReader Input; public TextWriter Output; 

Implementasi juru bahasa menurut skema di atas akan berada dalam satu fungsi Penerjemah ().

 void Interpreter() { while (true) { var word = ReadWord(Input); if (string.IsNullOrWhiteSpace(word)) return; // EOF var lookup = LookUp(word); if (IsEvalMode) { if (lookup != null) { Execute(lookup.Address); } else if (IsConstant(word)) { DS.Push(ParseNumber(word)); } else { DS.Clear(); Output.WriteLine($"The word {word} is undefined"); } } else { // program mode if (lookup != null) { if (lookup.Immediate) { Execute(lookup.Address); } else { AddOp(lookup.Address); } } else if (IsConstant(word)) { AddOp(LookUp("doLit").Address); AddOp(ParseNumber(word)); } else { IsEvalMode = true; DS.Clear(); Output.WriteLine($"The word {word} is undefined"); } } } } 

Interpretasi dilakukan dalam satu lingkaran, output yang dilakukan setelah mencapai akhir aliran input (misalnya, akhir file), sedangkan fungsi ReadWord akan mengembalikan string kosong. Tugas ReadWord adalah mengembalikan kata berikutnya dengan setiap panggilan.

 static string ReadWord(TextReader sr) { var sb = new StringBuilder(); var code = sr.Read(); while (IsWhite((char)code) && code > 0) { code = sr.Read(); } while (!IsWhite((char)code) && code > 0) { sb.Append((char)code); code = sr.Read(); } return sb.ToString(); } static bool IsWhite(char c) { return " \n\r\t".Any(ch => ch == c); } 

Setelah kata telah dibaca, dilakukan upaya untuk menemukannya di kamus. Jika berhasil, judul kata dikembalikan, jika tidak, null.

 public WordHeader LookUp(string word) { if (Entries.ContainsKey(word)) { return Entries[word].Last(); } return null; } 

Anda dapat memeriksa apakah nilai yang dimasukkan adalah angka dengan dua karakter pertama. Jika karakter pertama adalah angka, maka kita mengasumsikan bahwa itu adalah angka. Jika karakter pertama adalah tanda "+" atau "-", dan yang kedua adalah angka, kemungkinan besar ini juga angka.

 static bool IsConstant(string word) { return IsDigit(word[0]) || (word.Length >= 2 && (word[0] == '+' || word[0] == '-') && IsDigit(word[1])); } 

Untuk mengonversi string ke angka, Anda dapat menggunakan metode standar Int32.TryParse dan Double.TryParse. Tetapi kecepatan mereka tidak berbeda karena sejumlah alasan, jadi saya menggunakan solusi khusus.

 static object ParseNumber(string str) { var factor = 1.0; var sign = 1; if (str[0] == '-') { sign = -1; str = str.Remove(0, 1); } else if (str[0] == '+') { str = str.Remove(0, 1); } for (var i = str.Length - 1; i >= 0; i--) { if (str[i] == '.') { str = str.Remove(i, 1); return IntParseFast(str) * factor * sign; } factor *= 0.1; } return IntParseFast(str) * sign; } static int IntParseFast(string value) { // An optimized int parse method. var result = 0; foreach (var c in value) { if (!(c >= '0' && c <= '9')) return result; // error result = 10 * result + (c - 48); } return result; } 

Metode ParseNumber dapat mengonversi nilai integer dan angka floating point, misalnya, "1.618".

Eksekusi kata terjadi dengan cara yang sama seperti yang kita gunakan untuk menjalankan penerjemah alamat. Jika ada pengecualian, jejak tumpukan penerjemah alamat akan dicetak.

 public void Execute(int address) { try { if (address < Core.Count) { // eval core Core[address](); // invoke core function } else { // eval word IP = 0; // set return address WP = address; // set eval address DoList(); // fake doList Next(); // run evaluator } } catch (Exception e) { Output.WriteLine(e.Message); var wpEntry = Entries.FirstOrDefault(d => d.Value.Any(en => en.Address == WP)); var ipEntry = Entries.FirstOrDefault(d => d.Value.Any(en => en.Address == SearchKnowAddress(IP))); Output.WriteLine($"WP = {WP:00000} - '{wpEntry.Key}', IP = {IP:00000} - '{ipEntry.Key}'"); if (RS.Any()) { Output.WriteLine("Stack trace..."); foreach (var a in RS) { var ka = SearchKnowAddress(a); var sEntry = Entries.FirstOrDefault(d => d.Value.Any(en => en.Address == ka)); Output.WriteLine($"...{a:00000} -- {sEntry.Key}"); } RS.Clear(); DS.Clear(); } else if (address < Core.Count) { var entry = Entries.FirstOrDefault(d => d.Value.Any(en => en.Address == address)); Output.WriteLine($"Core word is {entry.Key}"); } IP = WP = 0; } } 

Ketika penerjemah dalam mode kompilasi dan kata itu tidak ditandai untuk segera dieksekusi, alamatnya harus ditulis ke memori.

 public void AddOp(object op) { Mem[Here++] = op; } 

Variabel di sini menyimpan alamat sel bebas berikutnya. Karena variabel ini harus dapat diakses dari lingkungan runtime sebagai variabel bahasa Fort, nilai di sini disimpan dalam memori program pada offset yang diberikan.

 public int _hereShift; public int Here { get => (int)Mem[_hereShift]; set => Mem[_hereShift] = value; } 

Untuk membedakan antara konstanta numerik dan alamat kata selama interpretasi, kompilasi kata doLit dikompilasi sebelum setiap konstanta selama kompilasi, yang membaca nilai berikutnya dalam memori dan menempatkannya pada tumpukan data.

 public void DoLit() { DS.Push(Mem[IP++]); } 

Kami telah menjelaskan penerjemah alamat dan teks, pengembangan lebih lanjut terdiri dalam mengisi inti atom. Versi berbeda dari "Fort" memiliki serangkaian kata dasar yang berbeda, implementasi yang paling minimalis adalah, mungkin, eForth, yang hanya berisi 31 primitif. Karena primitif berjalan lebih cepat daripada kata-kata pengguna majemuk, implementasi Fort minimal biasanya lebih lambat daripada implementasi verbose. Perbandingan kumpulan kata dari beberapa versi penerjemah dapat ditemukan di sini .

Dalam penerjemah yang dijelaskan di sini, saya juga mencoba untuk tidak perlu mengembang kamus kata-kata dasar. Tetapi untuk kemudahan integrasi dengan platform .net, saya memutuskan untuk mengimplementasikan matematika, operasi Boolean, dan, tentu saja, refleksi melalui serangkaian primitif. Pada saat yang sama, beberapa kata yang seringkali primitif dalam implementasi Fort tidak ada di sini, menyiratkan implementasi melalui penerjemah.

Pada saat penulisan, set dasar adalah 68 kata.
 // Core SetCoreWord("nop", Nop); SetCoreWord("next", Next); SetCoreWord("doList", DoList); SetCoreWord("exit", Exit); SetCoreWord("execute", Execute); SetCoreWord("doLit", DoLit); SetCoreWord(":", BeginDefWord); SetCoreWord(";", EndDefWord, true); SetCoreWord("branch", Branch); SetCoreWord("0branch", ZBranch); SetCoreWord("here", GetHereAddr); SetCoreWord("quit", Quit); SetCoreWord("dump", Dump); SetCoreWord("words", Words); SetCoreWord("'", Tick); SetCoreWord(",", Comma); SetCoreWord("[", Lbrac, true); SetCoreWord("]", Rbrac); SetCoreWord("immediate", Immediate, true); // Mem SetCoreWord("!", WriteMem); SetCoreWord("@", ReadMem); SetCoreWord("variable", Variable); SetCoreWord("constant", Constant); // RW SetCoreWord(".", Dot); SetCoreWord(".s", DotS); SetCoreWord("cr", Cr); SetCoreWord("bl", Bl); SetCoreWord("word", ReadWord, true); SetCoreWord("s\"", ReadString, true); SetCoreWord("key", Key); // Comment SetCoreWord("(", Comment, true); SetCoreWord("\\", CommentLine, true); // .net mem SetCoreWord("null", Null); SetCoreWord("new", New); SetCoreWord("type", GetType); SetCoreWord("m!", SetMember); SetCoreWord("m@", GetMember); SetCoreWord("ms@", GetStaticMember); SetCoreWord("ms!", SetStaticMember); SetCoreWord("load-assembly", LoadAssembly); SetCoreWord("invk", invk); // Boolean SetCoreWord("true", True); SetCoreWord("false", False); SetCoreWord("and", And); SetCoreWord("or", Or); SetCoreWord("xor", Xor); SetCoreWord("not", Not); SetCoreWord("invert", Invert); SetCoreWord("=", Eql); SetCoreWord("<>", NotEql); SetCoreWord("<", Less); SetCoreWord(">", Greater); SetCoreWord("<=", LessEql); SetCoreWord(">=", GreaterEql); // Math SetCoreWord("-", Minus); SetCoreWord("+", Plus); SetCoreWord("*", Multiply); SetCoreWord("/", Devide); SetCoreWord("mod", Mod); SetCoreWord("1+", Inc); SetCoreWord("1-", Dec); // Stack SetCoreWord("drop", Drop); SetCoreWord("swap", Swap); SetCoreWord("dup", Dup); SetCoreWord("over", Over); SetCoreWord("rot", Rot); SetCoreWord("nrot", Nrot); 


Untuk mendefinisikan kata-kata pengguna baru, dua kata kernel digunakan: ":" dan ";". Kata ":" membaca nama kata baru dari aliran input, membuat header dengan kunci ini, alamat kata dasar doList ditambahkan ke memori program, dan penerjemah dimasukkan ke dalam mode kompilasi. Semua kata berikutnya akan dikompilasi, dengan pengecualian yang ditandai sebagai langsung.

 public void BeginDefWord() { AddHeader(ReadWord(Input)); AddOp(LookUp("doList").Address); IsEvalMode = false; } 

Kompilasi diakhiri dengan kata ";", yang menuliskan alamat kata "keluar" ke dalam memori program dan memasukkannya ke mode interpretasi. Sekarang Anda dapat mendefinisikan kata-kata khusus - misalnya, loop, pernyataan kondisional, dan lainnya.

 Eval(": ? @ . ;"); Eval(": allot here @ + here ! ;"); Eval(": if immediate doLit [ ' 0branch , ] , here @ 0 , ;"); Eval(": then immediate dup here @ swap - swap ! ;"); Eval(": else immediate [ ' branch , ] , here @ 0 , swap dup here @ swap - swap ! ;"); Eval(": begin immediate here @ ;"); Eval(": until immediate doLit [ ' 0branch , ] , here @ - , ;"); Eval(": again immediate doLit [ ' branch , ] , here @ - , ;"); Eval(": while immediate doLit [ ' 0branch , ] , here @ 0 , ;"); Eval(": repeat immediate doLit [ ' branch , ] , swap here @ - , dup here @ swap - swap ! ;"); Eval(": // immediate [ ' \\ , ] ;"); // C like comment 

Saya tidak akan menjelaskan sisa kata-kata standar di sini - ada cukup informasi tentang mereka di jaringan pada sumber daya tematik yang sesuai. Untuk berinteraksi dengan platform, saya mendefinisikan 9 kata:

  • "Null" - mendorong null ke tumpukan;
  • β€œType” - mendorong tipe kelas ke tumpukan β€œword TrueForth.MyClass type”;
  • "Baru" - mengambil jenis dari tumpukan, membuat turunan kelas dan meletakkannya di tumpukan, argumen konstruktor, jika ada, juga harus berada di tumpukan "kata TrueForth.MyClass ketik baru";
  • "M!" - mengambil instance objek, nama bidang, nilai dari tumpukan dan memberikan nilai ke bidang yang ditentukan;
  • "M @" - mengambil sebuah instance dari objek dari stack, nama field dan mengembalikan nilai field ke stack;
  • "Ms!" Dan "ms @" - mirip dengan yang sebelumnya, tetapi untuk bidang statis, alih-alih sebuah instance, harus ada tipe pada stack;
  • "Load-assembly" - mengambil dari stack, membiarkannya ke assembly dan memuat ke memori;
  • "Invk" - mengambil delegasi, argumen dari stack dan menyebutnya "1133 kata SomeMethod kata TrueForth.MyClass ketik new m @ invk".

Saya menjelaskan poin-poin utama implementasi bahasa Fort, implementasi ini tidak berupaya untuk mendukung standar ANSI untuk bahasa tersebut, karena tugasnya adalah mengimplementasikan mesin untuk membangun DSL, dan bukan untuk mengimplementasikan bahasa tujuan umum. Dalam kebanyakan kasus, juru bahasa yang dikembangkan cukup untuk membangun bahasa yang sederhana dari bidang subjek.

Ada beberapa cara untuk menggunakan juru bahasa di atas. Misalnya, Anda dapat membuat instance dari juru bahasa, dan kemudian mengirimkan skrip inisialisasi ke input, di mana kata-kata yang diperlukan ditentukan. Yang terakhir melalui refleksi berinteraksi dengan sistem.

 public static bool Init4Th() { Interpreter = new OForth(); if (File.Exists(InitFile)) { Interpreter.Eval(File.ReadAllText(InitFile)); return true; } else { Console.WriteLine($"  {InitFile}  !"); return false; } } 

Contoh Konfigurasi Sistem Distribusi Laporan

 ( *****   ***** ) word GetFReporter word ReportProvider.FlexReports.FReporterEntry type new m@ invk constant fr //       :  word ReportProvider.FlexReports.FDailyReport type new ; //       :  word AddReport fr m@ invk ; //          :  [ ' word , ] ; //   :  [ ' word , ] ; //   :  [ ' s" , ] ; //  ,      " :  ; //  :  dup [ ' word , ] swap word MailSql swap m! ; :  dup [ ' word , ] swap word XlsSql swap m! ; ( *****    ***** ) cr s"   " . cr cr    "  08:00  mail@tinkoff.ru   seizure.sql    ,    "  08:00  mail@tinkoff.ru   fixed-errors-top.sql   fixed-errors.sql         WO"  08:00  mail@tinkoff.ru   wo-wait-complect-dates.sql       "  07:30  mail@tinkoff.ru   top-previous-input-errors.sql   previous-input-errors.sql        "  10:00  mail@tinkoff.ru   collection-report.sql       BPM   "  08:00  mail@tinkoff.ru   bpm-inbox-report.sql       ScanDoc3   7 "  07:50  mail@tinkoff.ru   new-sd3-complects-prevew.sql   new-sd3-complects.sql  ( ******************************** ) cr s"  " . cr 

Anda dapat melakukan sebaliknya: melewatkan objek yang sudah jadi ke input penerjemah melalui tumpukan data dan kemudian berinteraksi dengan mereka melalui penerjemah. Sebagai contoh, saya melakukan untuk mengembalikan pengaturan perangkat untuk menerima pindaian dokumen, pemindai, webcam atau perangkat virtual (untuk debugging atau pelatihan). Dalam hal ini, set parameter, pengaturan, urutan inisialisasi perangkat yang berbeda sangat berbeda dan diselesaikan secara sepele melalui fort-interpreter.

 var interpreter = new OForth(); interpreter.DS.Push(this); // Push current instance on DataStack interpreter.Eval("constant arctium"); // Define constant with the instance if (File.Exists(ConfigName)) { interpreter.Eval(File.ReadAllText(ConfigName)); } 

Konfigurasi dibuat secara terprogram, ternyata seperti ini:

 s" @device:pnp:\\?\usb#vid_2b16&pid_6689&mi_00#6&1ef84f63&0&0000#{65e8773d-8f56-11d0-a3b9-00a0c9223196}\global" s" Doccamera" word Scanning.Devices.PhotoScanner.PhotoScannerDevice type new dup s" 3264x2448, FPS:20, BIT:24" swap word SetSnapshotMode swap m@ invk dup s" 1280x720, FPS:30, BIT:24" swap word SetPreviewMode swap m@ invk word SetActiveDevice arctium m@ invk 

By the way, skrip * .ps dan * .pdf dihasilkan dengan cara yang sama, karena baik PostScript dan Pdf pada dasarnya adalah bagian dari "Fort", tetapi mereka digunakan secara eksklusif untuk merender dokumen pada layar atau printer.

Sama mudahnya untuk mengimplementasikan mode interaktif untuk konsol dan tidak hanya aplikasi. Untuk melakukan ini, pertama-tama Anda harus menginisialisasi sistem melalui skrip yang disiapkan, kemudian mulai interpretasi dengan menetapkan penerjemah pada input standar STDIN.

 var interpreter = new OForth(); const string InitFile = "Init.4th"; if (File.Exists(InitFile)) { interpreter.Eval(File.ReadAllText(InitFile)); } else { Console.WriteLine($"  {InitFile}  !"); } interpreter.Eval(Console.In); // Start interactive console 

Script inisialisasi dapat seperti ini:

 ( *****   ***** ) word ComplectBuilder.Program type constant main //     : mode! [ ' word , ] word Mode main ms! ; //    : init word Init main ms@ invk ; //  : load [ ' word , ] word LoadFile main ms@ invk ; //   : start word StartProcess main ms@ invk ; //   : count word Count main ms@ invk ; //   : all count ; //  ( *****  ***** ) init cr cr s"    ,     help" . cr cr ( *****  ***** ) : help s"         :" . cr s" load scandoc_test.csv 0 all start" . cr bl bl s" load scandoc_test.csv --    " . cr bl bl s" 0 all start --  ,  0      all " . cr cr s"     DEV TEST PROD:" . cr s" mode! DEV init" . cr s"     :" . cr s" word Mode main ms@ . cr" . cr ; 

Sebagai input, tidak hanya ada konsol atau teks dari aplikasi TextBox dengan UI, tetapi juga jaringan. Dalam hal ini, Anda dapat menerapkan kontrol interaktif sederhana, misalnya, layanan, untuk debugging, memulai, menghentikan komponen. Kemungkinan penggunaan tersebut dibatasi oleh imajinasi pengembang dan tugas yang dihadapi. , UI - .

. , , .

, :

 public void Callback(string word, MulticastDelegate action) { if (string.IsNullOrWhiteSpace(word) || word.Any(c => " \n\r\t".Any(cw => cw == c))) { throw new Exception("invalid format of word"); } DS.Push(action); Eval($": {word} [ ' doLit , , ] invk ;"); } 

DS.Push(action), . , , [ ], , . ' Tick , doLit, , . Comma Β«,Β» doLit, .

, . , :

 public class WoConfItem { public string ComplectType; public string Route; public string Deal; public bool IsStampQuery; } 

β€” , :

 public class WoConfig { private OForth VM; private List<WoConfItem> _conf; public WoConfig(string confFile) { _conf = new List<WoConfItem>(); VM = new OForth(); //      VM.Callback("new-conf", new Action(ClearConf)); VM.Callback("{", new Func<WoConfItem>(NewConf)); VM.Callback("}", new Action<WoConfItem>(AddConf)); VM.Callback("complect-type", new Func<WoConfItem,string,WoConfItem>(ConfComplectType)); VM.Callback("route", new Func<WoConfItem,string,WoConfItem>(ConfRoute)); VM.Callback("deal", new Func<WoConfItem,string,WoConfItem>(ConfDeal)); VM.Callback("is-stamp-query", new Func<WoConfItem,bool,WoConfItem>(ConfIsStampQuery)); //  ,   ,       var initScript = new StringBuilder(); initScript.AppendLine(": complect-type [ ' word , ] swap complect-type ;"); initScript.AppendLine(": route [ ' word , ] swap route ;"); initScript.AppendLine(": deal [ ' word , ] swap deal ;"); initScript.AppendLine(": is-stamp-query ' execute swap is-stamp-query ;"); VM.Eval(initScript.ToString()); //   WatchConfig(confFile); } private void ReadConfig(string path) { using (var reader = new StreamReader(File.OpenRead(path), Encoding.Default)) { VM.Eval(reader); } } readonly Func<string, bool> _any = s => s == "*"; public WoConfItem GetConf(string complectType, string routeId) { return _conf?.FirstOrDefault(cr => (cr.ComplectType == complectType || _any(cr.ComplectType)) && (cr.Route == routeId || _any(cr.Route)) ); } public bool IsAllow(string complectType, string routeId) { return GetConf(complectType, routeId) != null; } void WatchConfig(string path) { var directory = Path.GetDirectoryName(path); var fileName = Path.GetFileName(path); //   ,     if (!File.Exists(path)) { if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } var sb = new StringBuilder(); sb.AppendLine("\\ WO passport configuration"); sb.AppendLine("new-conf"); sb.AppendLine(""); sb.AppendLine("\\ Config rules"); sb.AppendLine("\\ { -- begin config item, } -- end config item, * -- match any values"); sb.AppendLine("\\ Example:"); sb.AppendLine("\\ { complect-type * route offer deal 100500 is-stamp-query true }"); sb.AppendLine(""); File.WriteAllText(path, sb.ToString(), Encoding.Default); } //   ReadConfig(path); //     var fsWatcher = new FileSystemWatcher(directory, fileName); fsWatcher.Changed += (sender, args) => { try { fsWatcher.EnableRaisingEvents = false; //        , //     ,   //     Thread.Sleep(1000); ReadConfig(path); } catch (Exception e) { Console.WriteLine(e); } finally { fsWatcher.EnableRaisingEvents = true; } }; fsWatcher.EnableRaisingEvents = true; } //  ,    void ClearConf() { _conf.Clear(); } void AddConf(WoConfItem conf) { _conf.Add(conf); } static WoConfItem NewConf() { return new WoConfItem(); } static WoConfItem ConfComplectType(WoConfItem conf, string complectType) { conf.ComplectType = complectType; return conf; } static WoConfItem ConfRoute(WoConfItem conf, string route) { conf.Route = route; return conf; } static WoConfItem ConfDeal(WoConfItem conf, string deal) { conf.Deal = deal; return conf; } static WoConfItem ConfIsStampQuery(WoConfItem conf, bool isStampQuery) { conf.IsStampQuery = isStampQuery; return conf; } } 


:

 \ WO passport configuration new-conf \ Config rules \ { -- begin config item, } -- end config item, * -- match any values \ Example: \ { complect-type * route offer deal 100500 is-stamp-query true } \ ***** offer ***** { complect-type offer route offer is-stamp-query false deal 5c18e87bfeed2b0b883fd4df } { complect-type KVK route offer is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type offer-cred route offer is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type offer-dep route offer is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type quick-meeting route offer is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type exica route offer is-stamp-query true deal 5d03a894e2f5850001435492 } { complect-type reissue route offer is-stamp-query true deal 5d03a894e2f5850001435492 } \ ***** offer-flow ***** { complect-type KVK route offer-flow is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type offer-cred route offer-flow is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type offer-dep route offer-flow is-stamp-query true deal 5d03a8a1edf8af0001876df0 } { complect-type reissue route offer-flow is-stamp-query true deal 5d03a894e2f5850001435492 } 

, , DSL β€” .

, «». DSL.

, , β€” , , , , β€” . , .

β€” , . β€” , β€” !

, .

- .

Semoga beruntung

Source: https://habr.com/ru/post/id477902/


All Articles