2 poin oleh GN⁺ 2024-11-09 | 1 komentar | Bagikan ke WhatsApp
  • sqleibniz adalah alat analisis statis untuk memeriksa sintaks SQL dialek SQLite, keberadaan tabel·kolom·fungsi, dan kondisi runtime, sehingga tokenisasi dan parsing menjadi tahap inti
  • macro_rules! di Rust membantu membuat struct node AST, implementasi trait Node, dan pengujian berbasis tabel ala Go tanpa pengulangan
  • Dengan pola matches! dan match, percabangan tata bahasa seperti literal angka SQLite, identifier, simbol, dan EXPLAIN QUERY PLAN dapat dipetakan hampir langsung ke dalam kode
  • Operator is_some_and, map, map_or milik Option serta operator ? membuat pengecekan keberadaan nilai, transformasi, nilai default, dan propagasi error dalam pemrosesan input·aliran token menjadi ringkas
  • Iterator Rust digunakan untuk menghapus _ pada literal angka, memvalidasi karakter heksadesimal di dalam blob, dan menghitung posisi error, yang pada akhirnya membuat kode tokenisasi·parsing lebih mudah dibaca

Alur analisis sqleibniz

  • sqleibniz adalah alat analisis SQL yang sedang dikembangkan untuk dialek SQLite
  • Untuk input SQL, alat ini ditujukan melakukan pemeriksaan sintaks, verifikasi keberadaan tabel·kolom·fungsi, serta validasi kondisi yang digabungkan dengan runtime SQLite bawaan
  • Pesan error ditargetkan agar menyediakan konteks dan penjelasan, serta memungkinkan diagnostik tertentu untuk diabaikan
  • Alur analisis dimulai dari analisis leksikal/tokenisasi, lalu parsing SQL sesuai dokumentasi SQLite, dan berlanjut ke analisis struktur hasilnya
  • Setelah bagian analisis statis selesai, ada rencana untuk menulis server LSP untuk SQL

Menghilangkan pengulangan node AST dengan makro

  • Node AST di sqleibniz adalah struct yang menyimpan Token, dan semua node harus mengimplementasikan trait Node
  • Trait Node menggunakan std::fmt::Debug sebagai supertrait, sehingga hanya tipe yang memenuhi Debug yang dapat mengimplementasikan Node
  • Agar tidak perlu mengulang definisi struct dan implementasi fn token(&self) -> &Token untuk setiap node, makro node! menghasilkan keduanya
    • Nama node diterima melalui metavariabel ident
    • String dokumentasi diterima melalui metavariabel literal
    • Field tambahan ditangani berulang dalam bentuk $($field_name:ident:$field_type:ty),*
  • Node Literal hanya memiliki field token, sedangkan node Explain menambahkan field child: Option<Box<dyn Node>>
  • Komentar dokumentasi diteruskan ke compiler sebagai argumen makro dalam bentuk #[doc = $documentation], bukan ///

Menerapkan pengujian berbasis tabel ala Go dengan makro Rust

  • Seperti pengujian berbasis tabel di Go, pendekatan yang menjalankan array kasus input dan mengeksekusi tiap kasus sebagai pengujian terpisah direplikasi dengan makro Rust
  • Pengujian lexer menggunakan makro test_group_pass_assert! dan test_group_fail!
    • Pengujian yang lolos memasukkan input ke Lexer lalu membandingkan daftar tipe token dari hasil Lexer.run() dengan nilai yang diharapkan
    • Pengujian gagal memeriksa bahwa vektor token hasil kosong dan Lexer.errors memiliki setidaknya satu error
    • Saat cargo test dijalankan, tiap kasus memberikan umpan balik ok atau fail layaknya fungsi pengujian terpisah
  • Pengujian parser mengikuti struktur yang sama, tetapi setelah lexer dijalankan, Parser diinisialisasi lalu hasil parse() diperiksa
    • EXPLAIN VACUUM; dan EXPLAIN QUERY PLAN VACUUM; adalah kasus yang lolos
    • EXPLAIN; dan EXPLAIN QUERY PLAN; adalah kasus yang gagal
    • Kasus gagal ini memverifikasi syarat dalam tata bahasa SQLite sql-stmt bahwa setelah EXPLAIN harus ada sebuah pernyataan

Hal yang kurang nyaman dari macro_rules!

  • Dukungan rust-analyzer di dalam macro_rules! terbatas
    • Tidak ada IntelliSense yang sesungguhnya
    • Tidak ada go to definition
    • Tidak ada hover signature untuk literal dan struktur bahasa
  • cargo fmt tidak memformat atau mengindentasi isi macro_rules! maupun lokasi pemanggilan makro
  • treesitter dan chroma kadang kesulitan dengan syntax highlighting macro_rules!
  • Dokumentasi makro juga cenderung kurang memadai

matches! dan match yang menonjol dalam pencocokan karakter

  • Perbandingan karakter di lexer adalah dasar bagi pemrosesan lain, dan makro matches! serta pola match di Rust membuat bagian ini ringkas
  • Pengenalan angka SQLite ditulis dengan matches!
    • +, -
    • _
    • .
    • a..=f, A..=F
    • 0..=9
  • Pengenalan identifier juga dinyatakan seperti matches!(c, 'a'..='z' | 'A'..='Z' | '_' | '0'..='9')
  • Loop utama lexer membagi karakter saat ini dengan match
    • Karakter spasi dilewati
    • *, ;, ,, % dan lain-lain masing-masing menghasilkan token yang sesuai
    • Contoh penanganan simbol tak dikenal dihilangkan dan ditandai sebagai panic!("whoops")

Menangani struktur tata bahasa SQL dengan pencocokan token

  • Lexer mengubah aliran karakter menjadi aliran struct Token yang memiliki informasi posisi dan tipe, lalu parser mengonsumsinya untuk membangun AST
  • Enum Type mencakup Keyword, Ident, Number, String, Blob, Boolean, ParamName, Param, Dot, Asteriks, Semicolon, Percent, Comma, Eof, dan lainnya
  • sql_stmt_prefix adalah fungsi parser yang menangani pernyataan EXPLAIN dari dokumentasi SQLite
    • Jika token saat ini adalah Type::Keyword(Keyword::EXPLAIN), node Explain dibuat dan EXPLAIN dikonsumsi
    • Jika token berikutnya adalah QUERY, maka QUERY dan PLAN dikonsumsi berurutan
    • Setelah itu, pernyataan SQL yang sebenarnya diparse sebagai child
    • Jika bukan EXPLAIN, pemrosesan sql_stmt biasa dipanggil
  • literal_value membuat node Literal untuk string, angka, blob, boolean, serta literal berbasis keyword seperti NULL, CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP

Penandaan error dan pemanfaatan Option

  • Lexer dan parser menampilkan error kepada pengguna untuk kasus seperti titik koma di akhir pernyataan SQL yang hilang
  • Operator ? di Rust digunakan untuk penanganan dan propagasi error
  • Option::is_some_and dipakai untuk memeriksa apakah karakter berikutnya atau karakter saat ini ada sekaligus memenuhi kondisi
    • self.source.get(self.pos + 1).is_some_and(...)
    • self.source.get(self.pos).is_some_and(...)
  • Option::map dimanfaatkan untuk mengubah byte berikutnya dari input Vec<u8> menjadi char
  • Option::map_or digunakan dengan cara hanya membandingkan tipe jika token saat ini atau token berikutnya ada, dan mengembalikan false jika tidak ada

Memproses angka dan blob dengan iterator

  • Parsing angka SQLite mengizinkan _, tetapi parsing angka Rust tidak mengizinkannya, sehingga lexer mengonsumsinya bersama input lalu menghapusnya sebelum parsing
  • Proses ini ditulis sebagai rangkaian iterator
    • Mengambil byte slice
    • Mengubah tiap byte menjadi char
    • Memfilter hanya karakter yang bukan _
    • Mengumpulkannya menjadi String
  • Dalam situasi ini digunakan unwrap_or_default(), tetapi string kosong tetap tidak valid sebagai angka sehingga parser tetap akan gagal
  • Di Go, proses yang sama mengharuskan iterasi daftar karakter, menulis byte ke strings.Builder, lalu membuat string lagi
  • Blob SQLite mengizinkan data heksadesimal berbentuk x'<hex>', sehingga tiap karakter string diperiksa dengan chars().enumerate() untuk memastikan is_ascii_hexdigit()
    • enumerate digunakan untuk memperoleh informasi posisi karakter yang salah untuk penandaan error
    • Saat menemukan karakter heksadesimal yang tidak valid, error dibuat lalu pemrosesan dihentikan

1 komentar

 
GN⁺ 2024-11-09
Komentar Hacker News
  • Sampai dua bulan lalu pun saya mungkin berpikir sama seperti penulis, tetapi saya terus membentur batas kaku Rust, yaitu borrow checker
    Tipe data aljabar, misalnya enum dan pattern matching, memang sangat bagus, tetapi karena borrow checker dan pertimbangan memori level rendah, saya jadi lebih banyak menghabiskan waktu melawan borrow checker daripada mengerjakan masalah bahasa pemrograman yang merupakan inti proyek
    Jadi tokenisasi dan parsing memang baik-baik saja, tetapi interpreter dan pemeriksaan tipe menjadi menyakitkan, lalu saat mencari bahasa yang lebih cocok, setelah mempertimbangkan F#, Zig/C, dan Go, saya menemukan OCaml
    Sintaksnya terasa seperti Haskell yang ramah, dan tampak seperti Rust tanpa lifetime, jadi saya tertarik; compiler Rust pertama juga ditulis dengan OCaml dan bahasa ini juga terkenal di bidang bahasa pemrograman
    Saya masih dalam tahap belajar jadi sulit memberi penilaian yang adil, tetapi sejauh ini ini paling mendekati apa yang saya cari

    • Setiap kali topik Go muncul, entah kenapa saya selalu kesal
      Bahasa ini praktis dan cepat, bukan benar-benar level rendah, kompilasinya juga cepat, dan yang terpenting populer sehingga semua library sudah tersedia, jadi rasanya memang seharusnya dipakai
      Tetapi saya hampir secara tidak rasional membenci bahasanya sendiri, dan rasanya semua aspeknya jelek
      Ini bahasa yang dibuat orang-orang C pada 2009, dan bahkan menurut standar saat itu pun terasa seolah mereka tidak tahu hal-hal menarik dari 20 tahun terakhir dalam desain bahasa pemrograman
      Bahkan PHP tahun 2009 terasa lebih modern dan lebih dirancang dengan baik daripada Go, dan saya sulit menepis kesan bahwa Go juga tidak banyak membaik setelah itu
    • Saat menangani abstract syntax tree di Rust, menurut saya kuncinya adalah tidak menyimpan hal-hal seperti string di dalam tree
      Sebagai gantinya, lebih baik gunakan sesuatu yang memungkinkan cheap clone dan interning, seperti library string statis, dan untuk posisi teks sebaiknya cukup pakai indeks
      Jika memungkinkan, sebisa mungkin hindari menyimpan referensi
      Semakin banyak Anda menyimpan hal-hal yang cheap/free clone, semakin sedikit Anda harus bertarung dengan borrow checker, dan bila perlu Anda bisa beralih ke clone
      Untuk sisi interpreter yang nyata, library yang membantu manajemen memori dengan cara seperti arena cukup berguna
      Ini memang area yang sangat khusus, tetapi memberi performa dan kemudahan penggunaan sekaligus, dan proyek seperti Ruffle juga banyak memakai pola seperti ini
      Namun OCaml dan Haskell melakukan hal-hal semacam ini secara “gratis” berkat reference counting bawaan dan garbage collection, meski begitu saya tetap menyukai gagasan melaju sangat cepat dengan Rust
    • Saya banyak memakai Go selama setahun terakhir, tetapi saya rasa saya tidak akan memakainya untuk menulis parser
      Go lebih dekat ke C yang dimodernisasi, dan model yang ditawarkannya sangat sederhana
      Jika datang dari C#, justru karena kesederhanaannya bahasa ini terasa lebih sulit dipelajari; kelebihannya adalah beban konsepnya rendah, dan cocok untuk aplikasi kecil yang fokus jika Anda bisa menerima kompromi berupa sifatnya yang verbose
      Kalau diminta merekomendasikan, menurut saya F# bagus, atau C# modern juga oke
      Memang Microsoft terlibat, tetapi jika ingin hidup di dunia yang sama sekali tidak memakai apa pun buatan korporasi besar yang jahat, itu akan sulit
      Java, Go, Python, TypeScript/JavaScript, dan Swift juga semua masuk di sini, jadi pilihan yang tersisa hampir tidak ada
      Saya penasaran bagaimana pendapat Anda setelah memakai OCaml sekitar setahun
      Keluarga Haskell memang menarik, tetapi Haskell sendiri tidak memberi keuntungan yang sepadan dengan kurva belajarnya bagi saya, dan Rust pun mirip
      Saya sudah mendalami sistem tipe C# hingga mahir, tetapi saya tidak punya waktu untuk menggali Rust sedalam itu
    • Go juga banyak dipakai di sisi klien, dan di mobile dukungannya juga cukup baik berkat go-mobile
      Tentu ini menambah sekitar 10~20MB pada biner dan penggunaan memori, tetapi menurut standar sekarang itu hampir bukan apa-apa
      Misalnya, Tailscale tampaknya memakai Go sebagai lapisan WireGuard lintas platform pada aplikasi mobile dan desktop, dan kelihatannya bekerja dengan baik
      Saya tidak akan membuat UI native dengan Go, tetapi untuk pekerjaan level rendah bahasa ini sangat bagus
      TinyGo juga memungkinkan penulisan Go untuk mikrokontroler atau WebAssembly; memang ada banyak hal yang belum didukung, tetapi sebagian besar standard library tetap bisa dipakai
    • Saya tidak akan menyebut Go sebagai bahasa server-side
      Sebagai contoh, compiler Go sendiri juga ditulis dalam Go
      Berkat cross-compilation dan biner yang relatif kecil, deployment menjadi sangat mudah
      Namun memang benar bahwa Go kekurangan syntactic sugar, dan tidak cocok untuk pattern matching bergaya fungsional
  • Cara pendekatan parsing ini terlihat agak aneh, dan saya mendapat kesan penulisnya belum terlalu akrab dengan Rust maupun konsep bahasa pemrograman yang mendasarinya
    Kalau melihat beberapa hal, AST tampaknya akan jauh lebih sederhana bila didefinisikan sebagai tipe data aljabar
    Rasanya kecil kemungkinan tata bahasa sqlite tiba-tiba berkembang dengan menambah banyak node baru sehingga membutuhkan encoding rumit seperti ini
    Encoding saat ini terlihat seperti bentuk yang akan terpikir oleh seseorang yang terbiasa dengan pemrograman berorientasi objek tetapi belum terbiasa dengan tipe data aljabar
    Pernyataan “macro bekerja berbeda di kebanyakan bahasa, tetapi alasan utamanya adalah menghilangkan duplikasi kode dan mengurangi pengulangan” sebenarnya juga bisa diterapkan pada semua mekanisme abstraksi seperti fungsi
    Ciri yang mendefinisikan macro adalah bahwa ia dijalankan pada waktu kompilasi
    Jika ingin melihat cara menata parser dengan rapi, riset parser combinator bisa menjadi titik awal yang baik

    • Penulis tidak pernah mengklaim dirinya programmer berpengalaman
      Judul blognya juga “Why I love ...”, dan meskipun poin yang disampaikan tampak valid, rasanya tidak perlu menyoroti kurangnya pengalaman
      Bahwa seseorang menyukai pemrograman adalah hal yang baik, dan pengalaman akan mengikuti
    • Dalam konteks tulisan blog itu, yang ingin dilakukan adalah menghasilkan definisi struct
      Ini tidak bisa dilakukan dengan fungsi
  • Dari sudut pandang seseorang yang pernah menulis parser kecil untuk notasi catur Forsyth-Edwards [0], Haskell terasa jauh lebih unggul dalam hal kesederhanaan dan keterbacaan
    Hampir terbaca seperti BNF dan nyaris tidak ada ritual teknis, jadi bisa fokus pada tata bahasa yang benar-benar ingin di-parse
    [0] https://github.com/ryandv/chesskell/blob/master/src/Chess/Fa...
    [1] https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notati...

    • Haskell memang yang terbaik untuk memanfaatkan parser combinator, tetapi untuk menangani hasil akhirnya tetap harus bekerja dengan Haskell
    • Bukankah ini bukan hanya memakai Haskell murni, melainkan juga library parser combinator?
      Saya penasaran apakah ada alasan yang jelas mengapa pendekatan serupa tidak bisa dilakukan di Rust
      Misalnya, winnow [1] tampaknya menyediakan gaya yang cukup deklaratif, dan Rust juga punya beberapa library parser combinator lain
      [1]: https://docs.rs/winnow/latest/winnow/
    • Saya tidak menganggap FEN sebagai contoh parsing yang bagus
      Karena itu bisa diimplementasikan sebagai fungsi sederhana dengan satu loop
      Beberapa hari lalu saya menulis “parser” FEN untuk implementasi quad-bitboard eksperimental, dan hampir terasa seperti menulis dirinya sendiri
      Sebagai tambahan, saya adalah penulis chessIO di Hackage
  • Saya pernah menulis disassembler eBPF dan emulator setengah jadi dengan Rust, dan Rust terasa cukup menyenangkan untuk pekerjaan jenis parsing
    Hanya saja, ketika penulis membutuhkan makro bahkan sebelum melewati 1/6 studi kasusnya, menurut saya itu sedikit melemahkan argumennya
    Makro memang bukan code generation penuh, tetapi juga tidak terlalu terasa seperti bekerja secara idiomatis di dalam bahasa itu sendiri
    Bukan bermaksud mencela, karena menurut saya Rust memang cukup kuat di area ini

    • Bagaimana Rust mendefinisikan grammar tak hingga?
      Misalnya, aturan context-free S ::= abc|aabbcc|aaabbbccc|... dapat secara efektif meng-parse a^Nb^Nc^N, dan ini adalah contoh grammar context-sensitive
      Ini memang contoh sederhana, tetapi hal serupa juga muncul di dunia nyata, misalnya ketika sebuah bahasa mengizinkan definisi operator
      Bagaimana Rust menangani hal seperti ini?
    • Akan bagus kalau Anda bisa membagikan tautan ke disassembler eBPF itu
      Kelihatannya keren
  • Dari pengalaman menulis parser dan lexer dengan Ragel serta memakai Go, Java, C++, dan C, menurut saya C murni pun bisa sebagus kode Rust yang dijelaskan penulis asalkan ada generator boilerplate secukupnya
    Bahkan mungkin lebih baik karena kesederhanaannya
    Misalnya, sebagian besar kode yang dibutuhkan untuk parser JSON hanya sekitar ini
    https://github.com/gritzko/librdx/blob/master/JSON.lex
    Sebenarnya eBNF itu hanya membuat lexer, dan bagian parser-nya juga tidak terlalu mengesankan, hanya 120 baris dan cukup repetitif
    https://github.com/gritzko/librdx/blob/master/JSON.c
    Pada akhirnya, menurut saya infrastruktur parser akan berevolusi sampai titik di mana eBNF saja sudah cukup untuk membuat parser, dan itu adalah titik jenuhnya

    • Repetitif seperti itu bisa dilihat sebagai kekurangan, bukan kelebihan
      Saya merasa algebraic data type di Rust membuat penanganan pohon sintaks yang dihasilkan jauh lebih mudah
      Meski begitu, saya setuju bahwa sedikit code generation atau sihir makro bisa membuat C jauh lebih nyaman dipakai
    • Saya benar-benar suka Ragel
      Tapi bukankah kode di sini
      https://github.com/gritzko/librdx/blob/master/JSON.lex
      menerima [ sebagai JSON yang valid?
      delimiter = OpenObject | CloseObject | OpenArray | CloseArray | Comma | Colon;
      primitive = Number | String | Literal;
      JSON = ws* ( primitive? ( ws* delimiter ws* primitive? )* ) ws*;
      Root = JSON;
      Sepertinya di JSON bisa memilih satu delimiter saja lalu memilih nol untuk semua sisanya
      Biasanya saya mulai dengan melihat RFC
      https://datatracker.ietf.org/doc/html/rfc4627#autoid-3
      Saya juga tidak yakin apakah JSON bisa diimplementasikan dengan Ragel
      Setahu saya Ragel hanya bisa menangani bahasa reguler, sedangkan JSON adalah bahasa context-free
    • Penyebab Cloudbleed adalah bug C/Ragel, dan itu juga alasan Cloudflare beralih ke Rust
      https://en.wikipedia.org/wiki/Cloudbleed
  • Terkait ini, saya suka kuliah Rob Pike tentang pemindaian leksikal
    Pendekatannya edukatif dan elegan
    https://www.youtube.com/watch?v=HxaD_trXwRE

    • Kuliah itu bagus, tetapi saya ingat pernah melihat pembahasan bahwa belakangan Go sebenarnya tidak memakai teknik tersebut
      Sepertinya alasannya karena overhead penjadwalan goroutine atau pola alokasi memori yang tidak efisien
      Pembahasan terbaik yang saya temukan adalah [1]
      Kuliah bagus lainnya tentang membuat lexer dan parser yang efisien adalah “Practical Data Oriented Design” oleh Andrew Kelley [2]
      Ringkasnya, kuliah itu menjelaskan berbagai strategi untuk meningkatkan throughput dengan mengurangi penggunaan memori program dan membuatnya ramah cache
      1: https://news.ycombinator.com/item?id=31649617
      2: https://www.youtube.com/watch?v=IroPQ150F6c
    • Kuliah itu tampaknya lebih terkait dengan cara mengekspresikan konkurensi pada masalah yang secara alami mengundang pemikiran tentang konkurensi, daripada tentang lexing itu sendiri
  • Ada satu pengalaman yang cukup mengejutkan bagi saya
    Saya bisa memakai library parser combinator yang saya gunakan untuk parser compiler tingkat tinggi apa adanya di lingkungan no-std, mengompilasinya untuk mikrokontroler, lalu mendistribusikannya sebagai parser protokol berperforma tinggi di lingkungan embedded
    Benar-benar library yang sama persis
    Perbedaannya hanya mengurangi penggunaan String dan lebih banyak memakai &'static str
    Jadi, bermain-main dengan compiler ternyata cukup nyambung dengan kemampuan membuat parser protokol embedded

  • Saat menulis seluruh parser AST dengan Rust, bagian yang sulit adalah merepresentasikan hierarki tipe AST konkret, termasuk upcasting dan downcasting
    Saya akhirnya menemukan caranya, tetapi perlu trik tipe aneh seperti PhantomData dan macro
    Sepertinya di sini juga perlu macro yang cukup berlebihan
    Saya penasaran seperti apa pekerjaan terdahulu yang terkait dengan ini

    • Tipe data aljabar dan sintaks matching di Rust tampaknya bagus
      Setidaknya sampai harus berurusan dengan upcasting/downcasting
      Saya belum cukup berpengalaman dengan Rust untuk tahu apakah ada cara penanganan yang baik
      Mungkin trait dinamis bisa dipakai
    • Kalau ini open source, saya penasaran apakah ada repositori publiknya
  • Bagaimana kode macro seperti ini biasanya di-debug, atau dipahami oleh orang yang baru masuk ke codebase
    Melihat pemakaian macro node! dan definisi macronya saja, rasanya sulit memahami kode seperti apa yang sebenarnya dihasilkan
    Apakah biasanya cukup menjalankan contoh lalu melihat type hint yang muncul, apakah IDE menampilkan versi hasil ekspansi saat hover, atau kalau ingin benar-benar yakin harus merujuk ke kode hasil kompilasi
    Saya hanya bekerja dengan JS/TS jadi tidak menyentuh macro, maka saya penasaran seperti apa alur kerja seperti ini

    • Menjalankan $ cargo expand akan memperlihatkan kode hasilnya
      Rust sebenarnya terasa lebih seperti beberapa bahasa sekaligus, dengan “vanilla” Rust, macro deklaratif, dan macro prosedural yang masing-masing punya kemampuan dan dialek yang sedikit berbeda
      Seiring waktu Anda akan terbiasa menangani masing-masing
      Unit test juga menjadi ruang eksperimen yang bagus untuk memahami dampak perubahan macro
    • rust-analyzer, Rust LSP yang dipakai di VSCode dan sebagainya, bisa mengekspansi macro deklaratif maupun macro prosedural secara rekursif
      Tidak terlalu buruk, tetapi semakin sedikit macro prosedural di codebase biasanya semakin baik
      Macro deklaratif sedikit lebih mudah dipahami, dan jauh lebih mudah dipelihara serta diuji
      Saya juga merasakan hal serupa terhadap code generation yang opak di bahasa lain
  • Parsing tata bahasa sqlite memang butuh keberuntungan
    Beberapa tahun lalu, karena pekerjaan, saya harus menulis parser untuk subset sqlite yang cukup kecil
    Saya sangat menyukai sqlite dan itu selalu menjadi sumber inspirasi
    diagram rel sangat berguna
    https://www.sqlite.org/syntaxdiagrams.html
    Menurut saya parser generator lemon kurang mendapatkan apresiasi
    https://sqlite.org/src/doc/trunk/doc/lemon.html
    Dari sisi pemilihan bahasa, bahasa apa pun yang memiliki tipe data aljabar akan cocok
    Bahkan TypeScript pun bisa sangat baik untuk tujuan ini
    Dulu saya juga pernah menulis pengantar kecil tentang menulis parser manual dengan Rust
    https://www.nhatcher.com/post/a-rustic-invitation-to-parsing...