Bangau, Bagian 4: Menerapkan Pernyataan dan Penutup

Diterbitkan: 2022-03-11

Dalam upaya kami untuk membuat bahasa pemrograman yang ringan menggunakan C++, kami memulai dengan membuat tokenizer kami tiga minggu lalu, dan kemudian kami menerapkan evaluasi ekspresi dalam dua minggu berikutnya.

Sekarang, saatnya untuk menyelesaikan dan memberikan bahasa pemrograman lengkap yang tidak akan sekuat bahasa pemrograman dewasa tetapi akan memiliki semua fitur yang diperlukan, termasuk jejak yang sangat kecil.

Saya merasa lucu bagaimana perusahaan baru memiliki bagian FAQ di situs web mereka yang tidak menjawab pertanyaan yang sering ditanyakan tetapi pertanyaan yang ingin mereka tanyakan. Saya akan melakukan hal yang sama di sini. Orang-orang yang mengikuti pekerjaan saya sering bertanya kepada saya mengapa Bangau tidak mengkompilasi ke beberapa bytecode atau setidaknya beberapa bahasa perantara.

Mengapa Bangau Tidak Dikompilasi ke Bytecode?

Saya senang menjawab pertanyaan ini. Tujuan saya adalah mengembangkan bahasa skrip tapak kecil yang akan dengan mudah diintegrasikan dengan C++. Saya tidak memiliki definisi ketat tentang "jejak kecil", tetapi saya membayangkan kompiler yang cukup kecil untuk memungkinkan portabilitas ke perangkat yang kurang kuat dan tidak akan menghabiskan terlalu banyak memori saat dijalankan.

C++ Bangau

Saya tidak fokus pada kecepatan, karena saya pikir Anda akan membuat kode dalam C++ jika Anda memiliki tugas kritis waktu, tetapi jika Anda memerlukan semacam ekstensibilitas, maka bahasa seperti Stork dapat berguna.

Saya tidak mengklaim bahwa tidak ada bahasa lain yang lebih baik yang dapat menyelesaikan tugas serupa (misalnya, Lua). Akan sangat tragis jika mereka tidak ada, dan saya hanya memberi Anda gambaran tentang kasus penggunaan bahasa ini.

Karena akan disematkan ke C++, saya merasa berguna untuk menggunakan beberapa fitur C++ yang ada daripada menulis seluruh ekosistem yang akan melayani tujuan yang sama. Tidak hanya itu, saya juga menemukan pendekatan ini lebih menarik.

Seperti biasa, Anda dapat menemukan kode sumber lengkap di halaman GitHub saya. Sekarang, mari kita lihat lebih dekat kemajuan kita.

Perubahan

Hingga bagian ini, Bangau adalah produk yang sebagian lengkap, jadi saya tidak dapat melihat semua kekurangan dan kekurangannya. Namun, karena bentuknya lebih lengkap, saya mengubah hal-hal berikut yang diperkenalkan di bagian sebelumnya:

  • Fungsi bukan variabel lagi. Ada function_lookup terpisah di compiler_context sekarang. function_param_lookup diubah namanya menjadi param_lookup untuk menghindari kebingungan.
  • Saya mengubah cara fungsi dipanggil. Ada metode call di runtime_context yang mengambil argumen std::vector , menyimpan indeks nilai pengembalian lama, mendorong argumen ke tumpukan, mengubah indeks nilai pengembalian, memanggil fungsi, memunculkan argumen dari tumpukan, mengembalikan indeks nilai pengembalian lama, dan mengembalikan hasilnya. Dengan begitu, kita tidak perlu menyimpan tumpukan indeks nilai kembalian, seperti sebelumnya, karena tumpukan C++ berfungsi untuk tujuan itu.
  • Kelas RAII ditambahkan dalam compiler_context yang dikembalikan oleh panggilan ke fungsi anggota scope dan function . Masing-masing objek tersebut membuat local_identifier_lookup dan param_identifier_lookup baru, masing-masing, di konstruktornya dan mengembalikan status lama di destruktor.
  • Kelas RAII ditambahkan di runtime_context , dikembalikan oleh fungsi anggota get_scope . Fungsi itu menyimpan ukuran tumpukan di konstruktornya dan mengembalikannya di destruktornya.
  • Saya menghapus kata kunci const dan objek konstan secara umum. Mereka bisa berguna tetapi tidak mutlak diperlukan.
  • var kata kunci dihapus, karena saat ini tidak diperlukan sama sekali.
  • Saya menambahkan kata kunci sizeof , yang akan memeriksa ukuran array saat runtime. Mungkin beberapa programmer C++ akan menganggap pilihan nama itu menghujat, karena C++ sizeof berjalan dalam waktu kompilasi, tetapi saya memilih kata kunci itu untuk menghindari tabrakan dengan beberapa nama variabel umum - misalnya size .
  • Saya menambahkan kata kunci tostring , yang secara eksplisit mengubah apa pun menjadi string . Itu tidak bisa menjadi fungsi, karena kami tidak mengizinkan fungsi yang berlebihan.
  • Berbagai perubahan kurang menarik.

Sintaksis

Karena kami menggunakan sintaks yang sangat mirip dengan C dan bahasa pemrograman terkait, saya akan memberikan detail yang mungkin tidak jelas.

Deklarasi tipe variabel adalah sebagai berikut:

  • void , hanya digunakan untuk tipe pengembalian fungsi
  • number
  • string
  • T[] adalah larik yang menyimpan elemen bertipe T
  • R(P1,...,Pn) adalah fungsi yang mengembalikan tipe R dan menerima argumen bertipe P1 ke Pn . Masing-masing tipe tersebut dapat diawali dengan & jika diteruskan dengan referensi.

Deklarasi fungsi adalah sebagai berikut: [public] function R name(P1 p1, … Pn pn)

Jadi, itu harus diawali dengan function . Jika diawali dengan public , maka dapat dipanggil dari C++. Jika fungsi tidak mengembalikan nilai, ia akan mengevaluasi ke nilai default dari tipe pengembaliannya.

Kami mengizinkan -loop for deklarasi dalam ekspresi pertama. Kami juga mengizinkan if -pernyataan dan switch -pernyataan dengan ekspresi inisialisasi, seperti pada C++17. Pernyataan if -dimulai dengan if -block, diikuti oleh nol atau lebih elif -block, dan opsional, satu else -block. Jika variabel dideklarasikan dalam ekspresi inisialisasi pernyataan if -, variabel tersebut akan terlihat di setiap blok tersebut.

Kami mengizinkan nomor opsional setelah pernyataan break yang dapat memutuskan dari beberapa loop bersarang. Jadi Anda dapat memiliki kode berikut:

 for (number i = 0; i < 100; ++i) { for(number j = 0; j < 100; ++j) { if (rnd(100) == 0) { break 2; } } }

Juga, itu akan putus dari kedua loop. Angka itu divalidasi dalam waktu kompilasi. Betapa kerennya itu?

Penyusun

Banyak fitur ditambahkan di bagian ini, tetapi jika saya terlalu detail, saya mungkin akan kehilangan bahkan pembaca paling gigih yang masih mendukung saya. Oleh karena itu, saya sengaja akan melewatkan satu bagian yang sangat besar dari cerita - kompilasi.

Itu karena saya sudah menjelaskannya di bagian pertama dan kedua dari seri blog ini. Saya fokus pada ekspresi, tetapi kompilasi yang lain tidak jauh berbeda.

Namun, saya akan memberi Anda satu contoh. Kode ini mengkompilasi pernyataan while :

 statement_ptr compile_while_statement( compiler_context& ctx, tokens_iterator& it, possible_flow pf ) { parse_token_value(ctx, it, reserved_token::kw_while); parse_token_value(ctx, it, reserved_token::open_round); expression<number>::ptr expr = build_number_expression(ctx, it); parse_token_value(ctx, it, reserved_token::close_round); block_statement_ptr block = compile_block_statement(ctx, it, pf); return create_while_statement(std::move(expr), std::move(block)); }

Seperti yang Anda lihat, ini jauh dari rumit. Ini mem-parsing while , lalu ( , lalu membangun ekspresi angka (kami tidak memiliki boolean), dan kemudian mem-parsing ) .

Setelah itu, ia mengkompilasi pernyataan blok yang mungkin ada di dalam { dan } atau tidak (ya, saya mengizinkan blok pernyataan tunggal) dan pada akhirnya membuat pernyataan while .

Anda sudah akrab dengan dua argumen fungsi pertama. Yang ketiga, possible_flow , menunjukkan perintah pengubah aliran yang diizinkan ( continue , break , return ) dalam konteks yang sedang kita parsing. Saya dapat menyimpan informasi itu dalam objek jika pernyataan kompilasi adalah fungsi anggota dari beberapa kelas compiler , tetapi saya bukan penggemar berat kelas raksasa, dan kompiler pasti akan menjadi salah satu kelas tersebut. Melewati argumen ekstra, terutama yang tipis, tidak akan menyakiti siapa pun, dan siapa tahu, mungkin suatu hari kita akan dapat memparalelkan kode.

Ada aspek lain yang menarik dari kompilasi yang ingin saya jelaskan di sini.

Jika kita ingin mendukung skenario di mana dua fungsi saling memanggil, kita dapat melakukannya dengan cara C: dengan mengizinkan deklarasi maju atau memiliki dua fase kompilasi.

Saya memilih pendekatan kedua. Ketika definisi fungsi ditemukan, kami akan mengurai jenis dan namanya ke dalam objek bernama incomplete_function . Kemudian, kita akan melewatkan tubuhnya, tanpa interpretasi, dengan hanya menghitung tingkat bersarang dari kurung kurawal sampai kita menutup kurung kurawal pertama. Kami akan mengumpulkan token dalam proses, menyimpannya di incomplete_function , dan menambahkan pengenal fungsi ke compiler_context .

Setelah kami melewati seluruh file, kami akan mengkompilasi setiap fungsi secara lengkap, sehingga mereka dapat dipanggil pada saat runtime. Dengan begitu, setiap fungsi dapat memanggil fungsi lain dalam file dan dapat mengakses variabel global apa pun.

Variabel global dapat diinisialisasi dengan panggilan ke fungsi yang sama, yang membawa kita langsung ke masalah "ayam dan telur" lama segera setelah fungsi tersebut mengakses variabel yang tidak diinisialisasi.

Jika itu pernah terjadi, masalah ini diselesaikan dengan melemparkan runtime_exception —dan itu hanya karena saya baik. Franky, pelanggaran akses adalah yang paling sedikit yang bisa Anda dapatkan sebagai hukuman karena menulis kode tersebut.

Lingkup Global

Ada dua jenis entitas yang dapat muncul dalam lingkup global:

  • Variabel global
  • Fungsi

Setiap variabel global dapat diinisialisasi dengan ekspresi yang mengembalikan tipe yang benar. Inisialisasi dibuat untuk setiap variabel global.

Setiap penginisialisasi mengembalikan lvalue , sehingga mereka berfungsi sebagai konstruktor variabel global. Ketika tidak ada ekspresi yang disediakan untuk variabel global, penginisialisasi default dibangun.

Ini adalah fungsi anggota initialize di runtime_context :

 void runtime_context::initialize() { _globals.clear(); for (const auto& initializer : _initializers) { _globals.emplace_back(initializer->evaluate(*this)); } }

Itu dipanggil dari konstruktor. Ini menghapus wadah variabel global, seperti yang dapat disebut secara eksplisit, untuk mengatur ulang status runtime_context .

Seperti yang saya sebutkan sebelumnya, kita perlu memeriksa apakah kita mengakses variabel global yang tidak diinisialisasi. Oleh karena itu, ini adalah pengakses variabel global:

 variable_ptr& runtime_context::global(int idx) { runtime_assertion( idx < _globals.size(), "Uninitialized global variable access" ); return _globals[idx]; }

Jika argumen pertama bernilai false , runtime_assertion melempar runtime_error dengan pesan yang sesuai.

Setiap fungsi diimplementasikan sebagai lambda yang menangkap pernyataan tunggal, yang kemudian dievaluasi dengan runtime_context yang diterima fungsi tersebut.

Lingkup Fungsi

Seperti yang dapat Anda lihat dari kompilasi pernyataan while , kompiler dipanggil secara rekursif, dimulai dengan pernyataan blok, yang mewakili blok seluruh fungsi.

Berikut adalah kelas dasar abstrak untuk semua pernyataan:

 class statement { statement(const statement&) = delete; void operator=(const statement&) = delete; protected: statement() = default; public: virtual flow execute(runtime_context& context) = 0; virtual ~statement() = default; };

Satu-satunya fungsi selain yang default adalah execute , yang menjalankan logika pernyataan pada runtime_context dan mengembalikan flow , yang menentukan ke mana logika program akan pergi selanjutnya.

 enum struct flow_type{ f_normal, f_break, f_continue, f_return, }; class flow { private: flow_type _type; int _break_level; flow(flow_type type, int break_level); public: flow_type type() const; int break_level() const; static flow normal_flow(); static flow break_flow(int break_level); static flow continue_flow(); static flow return_flow(); flow consume_break(); };

Fungsi pembuat statis cukup jelas, dan saya menulisnya untuk mencegah flow tidak logis dengan break_level bukan nol dan tipe yang berbeda dari flow_type::f_break .

Sekarang, consume_break akan membuat aliran jeda dengan satu level jeda yang lebih sedikit atau, jika level jeda mencapai nol, aliran normal.

Sekarang, kita akan memeriksa semua jenis pernyataan:

 class simple_statement: public statement { private: expression<void>::ptr _expr; public: simple_statement(expression<void>::ptr expr): _expr(std::move(expr)) { } flow execute(runtime_context& context) override { _expr->evaluate(context); return flow::normal_flow(); } };

Di sini, simple_statement adalah pernyataan yang dibuat dari sebuah ekspresi. Setiap ekspresi dapat dikompilasi sebagai ekspresi yang mengembalikan void , sehingga simple_statement dapat dibuat darinya. Karena break atau continue atau return tidak dapat menjadi bagian dari ekspresi, simple_statement mengembalikan flow::normal_flow() .

 class block_statement: public statement { private: std::vector<statement_ptr> _statements; public: block_statement(std::vector<statement_ptr> statements): _statements(std::move(statements)) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const statement_ptr& statement : _statements) { if ( flow f = statement->execute(context); f.type() != flow_type::f_normal ){ return f; } } return flow::normal_flow(); } };

block_statement menyimpan std::vector dari pernyataan. Itu mengeksekusi mereka, satu per satu. Jika masing-masing mengembalikan aliran non-normal, ia segera mengembalikan aliran itu. Ini menggunakan objek lingkup RAII untuk memungkinkan deklarasi variabel lingkup lokal.

 class local_declaration_statement: public statement { private: std::vector<expression<lvalue>::ptr> _decls; public: local_declaration_statement(std::vector<expression<lvalue>::ptr> decls): _decls(std::move(decls)) { } flow execute(runtime_context& context) override { for (const expression<lvalue>::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return flow::normal_flow(); } };

local_declaration_statement mengevaluasi ekspresi yang membuat variabel lokal dan mendorong variabel lokal baru ke tumpukan.

 class break_statement: public statement { private: int _break_level; public: break_statement(int break_level): _break_level(break_level) { } flow execute(runtime_context&) override { return flow::break_flow(_break_level); } };

break_statement memiliki level istirahat yang dievaluasi dalam waktu kompilasi. Itu hanya mengembalikan aliran yang sesuai dengan level istirahat itu.

 class continue_statement: public statement { public: continue_statement() = default; flow execute(runtime_context&) override { return flow::continue_flow(); } };

continue_statement baru saja mengembalikan flow::continue_flow() .

 class return_statement: public statement { private: expression<lvalue>::ptr _expr; public: return_statement(expression<lvalue>::ptr expr) : _expr(std::move(expr)) { } flow execute(runtime_context& context) override { context.retval() = _expr->evaluate(context); return flow::return_flow(); } }; class return_void_statement: public statement { public: return_void_statement() = default; flow execute(runtime_context&) override { return flow::return_flow(); } };

return_statement dan return_void_statement keduanya mengembalikan flow::return_flow() . Satu-satunya perbedaan adalah bahwa yang pertama memiliki ekspresi yang dievaluasi ke nilai kembalian sebelum kembali.

 class if_statement: public statement { private: std::vector<expression<number>::ptr> _exprs; std::vector<statement_ptr> _statements; public: if_statement( std::vector<expression<number>::ptr> exprs, std::vector<statement_ptr> statements ): _exprs(std::move(exprs)), _statements(std::move(statements)) { } flow execute(runtime_context& context) override { for (size_t i = 0; i < _exprs.size(); ++i) { if (_exprs[i]->evaluate(context)) { return _statements[i]->execute(context); } } return _statements.back()->execute(context); } }; class if_declare_statement: public if_statement { private: std::vector<expression<lvalue>::ptr> _decls; public: if_declare_statement( std::vector<expression<lvalue>::ptr> decls, std::vector<expression<number>::ptr> exprs, std::vector<statement_ptr> statements ): if_statement(std::move(exprs), std::move(statements)), _decls(std::move(decls)) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const expression<lvalue>::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return if_statement::execute(context); } };

if_statement , yang dibuat untuk satu if -block, nol atau lebih elif -blocks, dan satu else -block (yang bisa kosong), mengevaluasi setiap ekspresinya hingga satu ekspresi bernilai 1 . Itu kemudian mengeksekusi blok itu dan mengembalikan hasil eksekusi. Jika tidak ada ekspresi yang bernilai 1 , itu akan mengembalikan eksekusi blok ( else ) terakhir.

if_declare_statement adalah pernyataan yang memiliki deklarasi sebagai bagian pertama dari if-clause. Itu mendorong semua variabel yang dideklarasikan ke tumpukan dan kemudian mengeksekusi kelas dasarnya ( if_statement ).

 class switch_statement: public statement { private: expression<number>::ptr _expr; std::vector<statement_ptr> _statements; std::unordered_map<number, size_t> _cases; size_t _dflt; public: switch_statement( expression<number>::ptr expr, std::vector<statement_ptr> statements, std::unordered_map<number, size_t> cases, size_t dflt ): _expr(std::move(expr)), _statements(std::move(statements)), _cases(std::move(cases)), _dflt(dflt) { } flow execute(runtime_context& context) override { auto it = _cases.find(_expr->evaluate(context)); for ( size_t idx = (it == _cases.end() ? _dflt : it->second); idx < _statements.size(); ++idx ) { switch (flow f = _statements[idx]->execute(context); f.type()) { case flow_type::f_normal: break; case flow_type::f_break: return f.consume_break(); default: return f; } } return flow::normal_flow(); } }; class switch_declare_statement: public switch_statement { private: std::vector<expression<lvalue>::ptr> _decls; public: switch_declare_statement( std::vector<expression<lvalue>::ptr> decls, expression<number>::ptr expr, std::vector<statement_ptr> statements, std::unordered_map<number, size_t> cases, size_t dflt ): _decls(std::move(decls)), switch_statement(std::move(expr), std::move(statements), std::move(cases), dflt) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const expression<lvalue>::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return switch_statement::execute(context); } };

switch_statement mengeksekusi pernyataannya satu per satu, tetapi pertama-tama melompat ke indeks yang sesuai yang didapatnya dari evaluasi ekspresi. Jika salah satu pernyataannya mengembalikan aliran non-normal, itu akan segera mengembalikan aliran itu. Jika memiliki flow_type::f_break , ia akan menggunakan satu jeda terlebih dahulu.

switch_declare_statement memungkinkan deklarasi di header-nya. Tak satu pun dari mereka memungkinkan deklarasi dalam tubuh.

 class while_statement: public statement { private: expression<number>::ptr _expr; statement_ptr _statement; public: while_statement(expression<number>::ptr expr, statement_ptr statement): _expr(std::move(expr)), _statement(std::move(statement)) { } flow execute(runtime_context& context) override { while (_expr->evaluate(context)) { switch (flow f = _statement->execute(context); f.type()) { case flow_type::f_normal: case flow_type::f_continue: break; case flow_type::f_break: return f.consume_break(); case flow_type::f_return: return f; } } return flow::normal_flow(); } };
 class do_statement: public statement { private: expression<number>::ptr _expr; statement_ptr _statement; public: do_statement(expression<number>::ptr expr, statement_ptr statement): _expr(std::move(expr)), _statement(std::move(statement)) { } flow execute(runtime_context& context) override { do { switch (flow f = _statement->execute(context); f.type()) { case flow_type::f_normal: case flow_type::f_continue: break; case flow_type::f_break: return f.consume_break(); case flow_type::f_return: return f; } } while (_expr->evaluate(context)); return flow::normal_flow(); } };

while_statement dan do_while_statement keduanya mengeksekusi pernyataan tubuh mereka sementara ekspresi mereka mengevaluasi ke 1 . Jika eksekusi mengembalikan flow_type::f_break , mereka mengkonsumsinya dan kembali. Jika mengembalikan flow_type::f_return , mereka mengembalikannya. Dalam kasus eksekusi normal, atau melanjutkan, mereka tidak melakukan apa-apa.

Ini mungkin tampak seolah-olah continue tidak memiliki efek. Namun, pernyataan batin terpengaruh olehnya. Jika itu, misalnya, block_statement , itu tidak mengevaluasi sampai akhir.

Saya merasa rapi bahwa while_statement diimplementasikan dengan C++ while , dan do-statement dengan C++ do-while .

 class for_statement_base: public statement { private: expression<number>::ptr _expr2; expression<void>::ptr _expr3; statement_ptr _statement; public: for_statement_base( expression<number>::ptr expr2, expression<void>::ptr expr3, statement_ptr statement ): _expr2(std::move(expr2)), _expr3(std::move(expr3)), _statement(std::move(statement)) { } flow execute(runtime_context& context) override { for (; _expr2->evaluate(context); _expr3->evaluate(context)) { switch (flow f = _statement->execute(context); f.type()) { case flow_type::f_normal: case flow_type::f_continue: break; case flow_type::f_break: return f.consume_break(); case flow_type::f_return: return f; } } return flow::normal_flow(); } }; class for_statement: public for_statement_base { private: expression<void>::ptr _expr1; public: for_statement( expression<void>::ptr expr1, expression<number>::ptr expr2, expression<void>::ptr expr3, statement_ptr statement ): for_statement_base( std::move(expr2), std::move(expr3), std::move(statement) ), _expr1(std::move(expr1)) { } flow execute(runtime_context& context) override { _expr1->evaluate(context); return for_statement_base::execute(context); } }; class for_declare_statement: public for_statement_base { private: std::vector<expression<lvalue>::ptr> _decls; expression<number>::ptr _expr2; expression<void>::ptr _expr3; statement_ptr _statement; public: for_declare_statement( std::vector<expression<lvalue>::ptr> decls, expression<number>::ptr expr2, expression<void>::ptr expr3, statement_ptr statement ): for_statement_base( std::move(expr2), std::move(expr3), std::move(statement) ), _decls(std::move(decls)) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const expression<lvalue>::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return for_statement_base::execute(context); } };

for_statement dan for_statement_declare diimplementasikan sama seperti while_statement dan do_statement . Mereka diwarisi dari kelas for_statement_base , yang melakukan sebagian besar logika. for_statement_declare dibuat ketika bagian pertama dari for -loop adalah deklarasi variabel.

C++ Stork: Implementasi Pernyataan

Ini semua adalah kelas pernyataan yang kita miliki. Mereka adalah blok bangunan dari fungsi kita. Ketika runtime_context dibuat, itu menyimpan fungsi-fungsi itu. Jika fungsi dideklarasikan dengan kata kunci public , fungsi tersebut dapat dipanggil dengan nama.

Itu menyimpulkan fungsionalitas inti Bangau. Segala sesuatu yang lain yang akan saya jelaskan adalah renungan yang saya tambahkan untuk membuat bahasa kita lebih berguna.

Tuple

Array adalah wadah homogen, karena dapat berisi elemen dari satu jenis saja. Jika kita menginginkan wadah yang heterogen, struktur segera muncul dalam pikiran.

Namun, ada wadah heterogen yang lebih sepele: tupel. Tuple dapat menyimpan elemen dari tipe yang berbeda, tetapi tipenya harus diketahui dalam waktu kompilasi. Ini adalah contoh deklarasi Tuple di Stork:

 [number, string] t = {22321, "Siveric"};

Ini mendeklarasikan pasangan number dan string dan menginisialisasinya.

Daftar inisialisasi dapat digunakan untuk menginisialisasi array juga. Ketika tipe ekspresi dalam daftar inisialisasi tidak cocok dengan tipe variabel, kesalahan kompilator akan terjadi.

Karena array diimplementasikan sebagai container dari variable_ptr , kami mendapatkan implementasi runtime dari tupel secara gratis. Ini adalah waktu kompilasi ketika kami memastikan jenis yang benar dari variabel yang terkandung.

Modul

Akan lebih baik untuk menyembunyikan detail implementasi dari pengguna Stork dan menyajikan bahasa dengan cara yang lebih ramah pengguna.

Ini adalah kelas yang akan membantu kita mencapai itu. Saya menyajikannya tanpa detail implementasi:

 class module { ... public: template<typename R, typename... Args> void add_external_function(const char* name, std::function<R(Args...)> f); template<typename R, typename... Args> auto create_public_function_caller(std::string name); void load(const char* path); bool try_load(const char* path, std::ostream* err = nullptr) noexcept; void reset_globals(); ... };

Fungsi load dan try_load akan memuat dan mengkompilasi skrip Bangau dari jalur yang diberikan. Pertama, salah satu dari mereka dapat melempar stork::error , tetapi yang kedua akan menangkapnya dan mencetaknya pada output, jika tersedia.

Fungsi reset_globals akan menginisialisasi ulang variabel global.

Fungsi add_external_functions dan create_public_function_caller harus dipanggil sebelum kompilasi. Yang pertama menambahkan fungsi C++ yang dapat dipanggil dari Stork. Yang kedua membuat objek yang dapat dipanggil yang dapat digunakan untuk memanggil fungsi Bangau dari C++. Ini akan menyebabkan kesalahan waktu kompilasi jika tipe fungsi publik tidak cocok dengan R(Args…) selama kompilasi skrip Stork.

Saya menambahkan beberapa fungsi standar yang dapat ditambahkan ke modul Stork.

 void add_math_functions(module& m); void add_string_functions(module& m); void add_trace_functions(module& m); void add_standard_functions(module& m);

Contoh

Berikut adalah contoh skrip Bangau:

 function void swap(number& x, number& y) { number tmp = x; x = y; y = tmp; } function void quicksort( number[]& arr, number begin, number end, number(number, number) comp ) { if (end - begin < 2) return; number pivot = arr[end-1]; number i = begin; for (number j = begin; j < end-1; ++j) if (comp(arr[j], pivot)) swap(&arr[i++], &arr[j]); swap (&arr[i], &arr[end-1]); quicksort(&arr, begin, i, comp); quicksort(&arr, i+1, end, comp); } function void sort(number[]& arr, number(number, number) comp) { quicksort(&arr, 0, sizeof(arr), comp); } function number less(number x, number y) { return x < y; } public function void main() { number[] arr; for (number i = 0; i < 100; ++i) { arr[sizeof(arr)] = rnd(100); } trace(tostring(arr)); sort(&arr, less); trace(tostring(arr)); sort(&arr, greater); trace(tostring(arr)); }

Berikut adalah bagian C++:

 #include <iostream> #include "module.hpp" #include "standard_functions.hpp" int main() { std::string path = __FILE__; path = path.substr(0, path.find_last_of("/\\") + 1) + "test.stk"; using namespace stork; module m; add_standard_functions(m); m.add_external_function( "greater", std::function<number(number, number)>([](number x, number y){ return x > y; } )); auto s_main = m.create_public_function_caller<void>("main"); if (m.try_load(path.c_str(), &std::cerr)) { s_main(); } return 0; }

Fungsi standar ditambahkan ke modul sebelum kompilasi, dan fungsi trace dan rnd digunakan dari skrip Stork. Fungsi yang greater juga ditambahkan sebagai etalase.

Script dimuat dari file “test.stk,” yang berada di folder yang sama dengan “main.cpp” (dengan menggunakan definisi preprocessor __FILE__ ), dan kemudian fungsi main dipanggil.

Dalam skrip, kami menghasilkan array acak, mengurutkan secara menaik dengan menggunakan komparator less , dan kemudian secara menurun dengan menggunakan komparator greater , yang ditulis dalam C++.

Anda dapat melihat bahwa kode tersebut dapat dibaca dengan sempurna oleh siapa saja yang fasih berbahasa C (atau bahasa pemrograman apa pun yang berasal dari C).

Apa yang Harus Dilakukan Selanjutnya?

Ada banyak fitur yang ingin saya terapkan di Stork:

  • Struktur
  • Kelas dan warisan
  • Panggilan antar-modul
  • fungsi lambda
  • Objek yang diketik secara dinamis

Kurangnya waktu dan ruang adalah salah satu alasan mengapa kami belum menerapkannya. Saya akan mencoba memperbarui halaman GitHub saya dengan versi baru saat saya menerapkan fitur baru di waktu luang saya.

Membungkus

Kami telah membuat bahasa pemrograman baru!

Itu menghabiskan sebagian besar waktu luang saya dalam enam minggu terakhir, tetapi sekarang saya dapat menulis beberapa skrip dan melihatnya berjalan. Itulah yang saya lakukan dalam beberapa hari terakhir, menggaruk kepala botak saya setiap kali tiba-tiba jatuh. Terkadang, itu adalah bug kecil, dan terkadang bug yang buruk. Namun, di lain waktu, saya merasa malu karena ini tentang keputusan buruk yang telah saya bagikan kepada dunia. Tetapi setiap kali, saya akan memperbaiki dan terus membuat kode.

Dalam prosesnya, saya belajar tentang if constexpr , yang belum pernah saya gunakan sebelumnya. Saya juga menjadi lebih akrab dengan nilai-referensi dan penerusan yang sempurna, serta dengan fitur-fitur kecil lainnya dari C++17 yang tidak saya temui setiap hari.

Kodenya tidak sempurna—saya tidak akan pernah membuat klaim seperti itu—tetapi cukup baik, dan sebagian besar mengikuti praktik pemrograman yang baik. Dan yang paling penting - itu berhasil.

Memutuskan untuk mengembangkan bahasa baru dari awal mungkin terdengar gila bagi rata-rata orang, atau bahkan bagi programmer biasa, tetapi itu adalah alasan untuk melakukannya dan membuktikan pada diri sendiri bahwa Anda bisa melakukannya. Sama seperti memecahkan teka-teki yang sulit adalah latihan otak yang baik untuk tetap sehat secara mental.

Tantangan yang membosankan adalah hal biasa dalam pemrograman sehari-hari kami, karena kami tidak dapat memilih hanya aspek yang menarik darinya dan harus melakukan pekerjaan yang serius meskipun terkadang membosankan. Jika Anda seorang pengembang profesional, prioritas pertama Anda adalah memberikan kode berkualitas tinggi kepada atasan Anda dan menyiapkan makanan. Hal ini terkadang dapat membuat Anda menghindari pemrograman di waktu senggang dan dapat mengurangi semangat awal Anda di sekolah pemrograman.

Jika Anda tidak perlu, jangan kehilangan antusiasme itu. Kerjakan sesuatu jika menurut Anda itu menarik, bahkan jika itu sudah selesai. Anda tidak perlu membenarkan alasan untuk bersenang-senang.

Dan jika Anda dapat memasukkannya—bahkan sebagian—dalam pekerjaan profesional Anda, bagus untuk Anda! Tidak banyak orang yang memiliki kesempatan itu.

Kode untuk bagian ini akan dibekukan dengan cabang khusus di halaman GitHub saya.