2 पॉइंट द्वारा GN⁺ 2024-11-09 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • sqleibniz SQLite dialect SQL की syntax, table·column·function की मौजूदगी और runtime conditions की जांच करने वाला static analysis tool है, और इसके लिए tokenization और parsing मुख्य चरण हैं
  • Rust का macro_rules! AST node structs, Node trait implementations, और Go-style table-based tests को बिना repetition के बनाने में मदद करता है
  • matches! और match patterns का उपयोग करने से SQLite numeric literals, identifiers, symbols, और EXPLAIN QUERY PLAN जैसी syntax branches को code के करीब ढंग से व्यक्त किया जा सकता है
  • Option के is_some_and, map, map_or और ? operator input·token stream processing में value की मौजूदगी, transformation, default values और error propagation को concise बनाते हैं
  • Rust iterators numeric literals से _ हटाने, blob के अंदर hexadecimal characters validate करने और error location calculate करने में उपयोग होते हैं, जिससे tokenization·parsing code अधिक readable बनता है

sqleibniz का analysis flow

  • sqleibniz SQLite dialect को target करके बनाया जा रहा SQL analysis tool है
  • SQL input पर syntax checking, table·column·function की मौजूदगी की जांच, और embedded SQLite runtime के साथ combined condition validation करने का लक्ष्य है
  • Error messages context और explanation देंगे, और कुछ specific diagnostics को ignore करने की सुविधा देने का लक्ष्य है
  • Analysis flow lexical analysis/tokenization से शुरू होकर SQLite documentation के अनुसार SQL parsing और result structure analysis तक जाता है
  • Static analysis part पूरा होने के बाद SQL के लिए LSP server लिखने की भी योजना है

Macro से AST node repetition हटाना

  • sqleibniz के AST nodes ऐसे structs हैं जिनमें Token होता है, और हर node को Node trait implement करना होता है
  • Node trait std::fmt::Debug को supertrait के रूप में उपयोग करता है, इसलिए केवल Debug satisfy करने वाले types ही Node implement कर सकते हैं
  • हर node के लिए struct definition और fn token(&self) -> &Token implementation दोहराने से बचने के लिए node! macro इन्हें generate करता है
    • Node name ident metavariable से लिया जाता है
    • Documentation string literal metavariable से लिया जाता है
    • Additional fields को $($field_name:ident:$field_type:ty),* form में repeat-process किया जाता है
  • Literal node में केवल token field होता है, और Explain node में अतिरिक्त child: Option<Box<dyn Node>> field होता है
  • Documentation comments /// के बजाय #[doc = $documentation] form में macro arguments compiler को pass करते हैं

Go-style table-based tests को Rust macro से implement करना

  • Go के table-based tests की तरह input cases array पर iterate करते हुए हर case को independent test के रूप में चलाने की शैली को Rust macro से reproduce किया गया है
  • Lexer tests test_group_pass_assert! और test_group_fail! macros का उपयोग करते हैं
    • Passing tests input को Lexer में डालते हैं और Lexer.run() result के token type list को expected values से compare करते हैं
    • Failing tests check करते हैं कि result token vector खाली है और Lexer.errors में कम से कम एक error है
    • cargo test चलाने पर हर case अलग test function की तरह ok या fail feedback देता है
  • Parser tests भी वही structure follow करते हैं, लेकिन lexer run के बाद Parser initialize करते हैं और parse() result check करते हैं
    • EXPLAIN VACUUM; और EXPLAIN QUERY PLAN VACUUM; passing cases हैं
    • EXPLAIN; और EXPLAIN QUERY PLAN; failing cases हैं
    • Failing cases SQLite sql-stmt grammar में EXPLAIN के बाद statement आवश्यक होने की condition verify करते हैं

macro_rules! की असुविधाएं

  • macro_rules! के अंदर rust-analyzer support limited है
    • वास्तविक IntelliSense नहीं है
    • Go to definition नहीं है
    • Literals और language structure signatures पर hover नहीं है
  • cargo fmt macro_rules! के अंदर और macro call sites को format या indent नहीं करता
  • treesitter और chroma कभी-कभी macro_rules! syntax highlighting में संघर्ष करते हैं
  • Macro documentation भी कुछ कम है

Character matching में चमकते matches! और match

  • Lexer में character comparison बाकी processing की नींव है, और Rust का matches! macro व match patterns इस हिस्से को concise बनाते हैं
  • SQLite number detection matches! से लिखा गया है
    • +, -
    • _
    • .
    • a..=f, A..=F
    • 0..=9
  • Identifier detection भी matches!(c, 'a'..='z' | 'A'..='Z' | '_' | '0'..='9') जैसे express किया जाता है
  • Lexer का main loop current character को match से branch करता है
    • Whitespace characters skip किए जाते हैं
    • *, ;, ,, % आदि के लिए संबंधित tokens बनाए जाते हैं
    • Unknown symbol handling example omit किया गया है और panic!("whoops") से दिखाया गया है

Token matching से SQL grammar structure संभालना

  • Lexer character stream को position information और type information वाले Token struct stream में बदलता है, और parser उसे consume करके AST बनाता है
  • Type enum में Keyword, Ident, Number, String, Blob, Boolean, ParamName, Param, Dot, Asteriks, Semicolon, Percent, Comma, Eof आदि शामिल हैं
  • sql_stmt_prefix SQLite documentation के EXPLAIN statement को handle करने वाला parser function है
    • अगर current token Type::Keyword(Keyword::EXPLAIN) है, तो Explain node बनाता है और EXPLAIN consume करता है
    • अगर next token QUERY है, तो QUERY और PLAN को consecutively consume करता है
    • इसके बाद actual SQL statement को child के रूप में parse करता है
    • अगर EXPLAIN नहीं है, तो general sql_stmt handling call करता है
  • literal_value strings, numbers, blob, boolean के साथ NULL, CURRENT_TIME, CURRENT_DATE, CURRENT_TIMESTAMP जैसे keyword literals को Literal node बनाता है

Error display और Option का उपयोग

  • Lexer और parser SQL statement के अंत में semicolon missing जैसी situations को user को error के रूप में दिखाते हैं
  • Rust का ? operator error handling और propagation में उपयोग होता है
  • Option::is_some_and तब इस्तेमाल होता है जब next character या current character मौजूद हो और condition satisfy करता हो या नहीं यह check करना हो
    • self.source.get(self.pos + 1).is_some_and(...)
    • self.source.get(self.pos).is_some_and(...)
  • Option::map Vec<u8> input में next byte को char में बदलने के लिए इस्तेमाल होता है
  • Option::map_or current token या next token होने पर ही type comparison करने और न होने पर false return करने के तरीके से इस्तेमाल होता है

Iterators से numbers और blob processing

  • SQLite number parsing _ allow करता है, लेकिन Rust number parsing _ allow नहीं करता, इसलिए lexer _ सहित consume करता है और parsing से पहले उसे हटा देता है
  • यह processing iterator chain से लिखी गई है
    • Byte slice लिया जाता है
    • हर byte को char में convert किया जाता है
    • _ न होने वाले characters ही filter किए जाते हैं
    • String में collect किया जाता है
  • इस situation में unwrap_or_default() इस्तेमाल होता है, लेकिन empty string number के रूप में valid नहीं होती, इसलिए parser वैसे भी fail होगा
  • Go में यही processing करने के लिए character list iterate करनी पड़ती, strings.Builder में bytes लिखने पड़ते और फिर दोबारा string बनानी पड़ती
  • SQLite blob x'<hex>' form में hexadecimal data allow करता है, इसलिए string के हर character को chars().enumerate() से iterate करते हुए is_ascii_hexdigit() check किया जाता है
    • enumerate invalid character की location information error display के लिए पाने में इस्तेमाल होता है
    • Invalid hexadecimal character मिलने पर error create करके processing रोक दी जाती है

1 टिप्पणियां

 
GN⁺ 2024-11-09
Hacker News टिप्पणियाँ
  • दो महीने पहले तक मैं भी लेखक जैसा ही सोचता, लेकिन Rust की सख्त सीमा यानी borrow checker से लगातार टकराता रहा
    Algebraic data types, जैसे Enum और pattern matching, सच में बहुत अच्छे लगे, लेकिन borrow checker और low-level memory considerations की वजह से प्रोजेक्ट के मुख्य programming language problem से ज़्यादा समय borrow checker से लड़ने में चला गया
    इसलिए tokenization और parsing तो ठीक रहे, लेकिन interpreter और type checking दर्द बन गए, और ज़्यादा उपयुक्त भाषा खोजते हुए F#, Zig/C, Go पर विचार करने के बाद OCaml मिला
    इसकी syntax friendly Haskell जैसी है, और यह lifetime-रहित Rust जैसा दिखा, इसलिए मैं आश्वस्त हुआ; पहला Rust compiler भी OCaml में लिखा गया था और programming languages के क्षेत्र में यह जाना-पहचाना है
    अभी सीख ही रहा हूँ, इसलिए निष्पक्ष मूल्यांकन मुश्किल है, लेकिन अब तक यह ठीक उसी चीज़ के करीब है जिसे मैं खोज रहा था

    • Go की बात आते ही पता नहीं क्यों हमेशा चिढ़ होती है
      यह practical और तेज़ है, सचमुच low-level नहीं है, compile भी तेज़ होता है, और सबसे बढ़कर लोकप्रिय है इसलिए libraries भी सब हैं, तो लगता है कि इसे इस्तेमाल करना चाहिए
      लेकिन भाषा खुद मुझे लगभग अतार्किक रूप से नापसंद है, और इसकी हर चीज़ बदसूरत लगती है
      यह 2009 में C वाले लोगों द्वारा बनाई गई भाषा है, लेकिन उस समय के हिसाब से भी ऐसा लगता है जैसे उन्हें पिछले 20 वर्षों में programming language design में हुई दिलचस्प चीज़ों का पता ही न हो
      2009 का PHP भी Go से ज़्यादा modern और बेहतर designed language था, और यह एहसास हटता नहीं कि Go उसके बाद भी खास बेहतर नहीं हुआ
    • Rust में abstract syntax tree संभालते समय मेरे हिसाब से मुख्य बात यह है कि strings जैसी चीज़ें tree में store न की जाएँ
      इसके बजाय static string library जैसी कोई चीज़ इस्तेमाल करें जिसमें cheap clone और interning संभव हो, और text positions के लिए सिर्फ़ indexes का इस्तेमाल करना बेहतर है
      जहाँ तक हो सके, references store करने से बिल्कुल बचना चाहिए
      जितना ज़्यादा आप cheap/free clone की जा सकने वाली चीज़ों के रूप में रखते हैं, borrow checker से उतना कम लड़ना पड़ता है, और ज़रूरत होने पर clone पर जा सकते हैं
      असली interpreter side में arena जैसे तरीक़ों से memory management में मदद करने वाली libraries काफ़ी उपयोगी होती हैं
      यह बहुत specialized क्षेत्र है, लेकिन performance और usability दोनों देता है, और Ruffle जैसे projects भी इस pattern का बहुत इस्तेमाल करते हैं
      हालांकि OCaml और Haskell built-in reference counting और garbage collection की वजह से ये चीज़ें “मुफ़्त” में कर देते हैं, फिर भी Rust से बहुत तेज़ जाने का विचार मुझे पसंद है
    • पिछले 1 साल में Go काफ़ी इस्तेमाल किया है, लेकिन parser लिखने के लिए शायद इस्तेमाल नहीं करूँगा
      Go modernized C के ज़्यादा करीब है, और इसका model बहुत सरल है
      C# से आने पर इसी सरलता की वजह से इसे सीखना उल्टा ज़्यादा कठिन लगा; कम conceptual burden इसका फायदा है और यह छोटी, focused applications के लिए अच्छा है जहाँ verbosity वाला trade-off स्वीकार किया जा सके
      सिफ़ारिश करनी हो तो F#, या modern C# भी ठीक लगता है
      Microsoft इसमें शामिल है, लेकिन अगर ऐसी दुनिया में रहना है जहाँ किसी evil big tech ने बनाई कोई चीज़ बिल्कुल नहीं इस्तेमाल करनी, तो मुश्किल हो जाएगा
      Java, Go, Python, TypeScript/JavaScript, Swift भी सभी इसमें आ जाते हैं, और तब विकल्प लगभग नहीं बचते
      लगभग 1 साल OCaml इस्तेमाल करने के बाद आपकी राय जानने में दिलचस्पी होगी
      Haskell-जैसी भाषाएँ रोचक हैं, लेकिन Haskell खुद में learning curve के मुकाबले फायदा मेरे लिए अच्छा नहीं था, और Rust भी वैसा ही है
      C# में type system में गहराई तक जाकर महारत हासिल की है, लेकिन Rust में उतनी गहराई तक जाने का समय नहीं है
    • Go client side पर भी काफ़ी इस्तेमाल होता है, और mobile में भी go-mobile की वजह से support काफ़ी अच्छा है
      बेशक यह binary और memory usage में लगभग 10–20MB जोड़ता है, लेकिन आज के मानकों से यह लगभग कुछ भी नहीं है
      उदाहरण के लिए Tailscale अपने mobile और desktop apps में cross-platform WireGuard layer के रूप में Go इस्तेमाल करता दिखता है, और यह अच्छी तरह काम करता लगता है
      Go से native UI नहीं बनाऊँगा, लेकिन low-level कामों के लिए यह शानदार है
      TinyGo microcontrollers या WebAssembly के लिए Go लिखना भी संभव बनाता है, और बहुत सी चीज़ें supported नहीं हैं, लेकिन standard library का बड़ा हिस्सा इस्तेमाल किया जा सकता है
    • Go को server-side language नहीं कहूँगा
      उदाहरण के लिए Go compiler भी Go में लिखा गया है
      cross-compile और अपेक्षाकृत छोटे binaries की वजह से deployment बहुत आसान है
      हालांकि syntax sugar की कमी वाली बात सही है, और functional style की pattern matching के लिए यह अच्छी तरह फिट नहीं बैठता
  • parsing को approach करने का तरीका थोड़ा अजीब दिखता है, और लेखक Rust और उसके आधार बनने वाले programming language concepts से अपेक्षाकृत अपरिचित लगते हैं
    कुछ बातें देखें तो, AST को algebraic data types के रूप में define किया जाए तो शायद यह कहीं ज़्यादा सरल हो जाएगा
    sqlite grammar अचानक नए nodes बहुत बढ़ाकर ऐसी तरह expand होगी कि complex encoding चाहिए, ऐसा नहीं लगता
    मौजूदा encoding ऐसी लगती है जैसी कोई व्यक्ति सोच सकता है जो object-oriented से परिचित हो लेकिन algebraic data types से नहीं
    “macros ज़्यादातर भाषाओं में अलग तरह से काम करते हैं, लेकिन मुख्य वजह code duplication हटाना और repetition कम करना है” — यह बात functions जैसे सभी abstraction mechanisms पर भी लागू की जा सकती है
    macro की defining characteristic यह है कि वह compile time पर execute होता है
    parser को साफ़-सुथरे ढंग से structure करने के तरीके देखने के लिए parser combinator research एक अच्छा starting point हो सकता है

    • लेखक ने कभी यह दावा नहीं किया कि वे बहुत अनुभवी programmer हैं
      blog का title भी “Why I love ...” है, और आलोचना अपने-आप में सही लगती है, लेकिन अनुभव की कमी बताना ज़रूरी नहीं लगता
      किसी का programming पसंद करना अच्छी बात है, और अनुभव समय के साथ आ जाएगा
    • blog post के context में वे struct definitions generate करना चाहते हैं
      यह function से नहीं किया जा सकता
  • Forsyth-Edwards chess notation के लिए एक छोटा parser [0] लिखने के अनुभव से, simplicity और readability के मामले में Haskell मुझे बेजोड़ लगता है
    यह लगभग BNF जैसा पढ़ा जाता है और technical ceremony बहुत कम है, इसलिए आप सच में जिस grammar को parse करना चाहते हैं उसी पर ध्यान दे सकते हैं
    [0] https://github.com/ryandv/chesskell/blob/master/src/Chess/Fa...
    [1] https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notati...

    • Haskell parser combinators का इस्तेमाल करने के लिहाज से निश्चित रूप से सबसे अच्छा है, लेकिन output को संभालना हो तो फिर भी Haskell के साथ ही रहना पड़ता है
    • यह pure Haskell ही नहीं, बल्कि parser combinator library का इस्तेमाल करके लिखा गया है, है ना
      सोच रहा हूँ कि Rust में वैसा approach न हो पाने की कोई स्पष्ट वजह है क्या
      उदाहरण के लिए winnow [1] काफी declarative style देता दिखता है, और Rust में कई अन्य parser combinator libraries भी हैं
      [1]: https://docs.rs/winnow/latest/winnow/
    • FEN मुझे parsing के लिए कोई बढ़िया example नहीं लगता
      क्योंकि इसे एक single loop वाले simple function से implement किया जा सकता है
      कुछ दिन पहले experimental quad-bitboard implementation के लिए FEN “parser” लिखा था, और वह लगभग अपने-आप लिख गया
      वैसे, मैं Hackage के chessIO का author हूँ
  • मैंने Rust में एक eBPF disassembler और आधा बना emulator लिखा है, और parsing-जैसे कामों के लिए Rust काफी pleasant language लगी
    हालांकि लेखक case study के 1/6 हिस्से तक भी नहीं पहुँचता कि उसे macro की जरूरत पड़ जाती है, तो मुझे लगता है कि इससे उसकी दलील कमजोर होती है
    macros पूरी code generation तो नहीं हैं, लेकिन भाषा के भीतर idiomatic तरीके से काम करने जैसा अहसास भी बहुत मजबूत नहीं होता
    यह आलोचना के लिए नहीं कह रहा; मुझे लगता है Rust सच में इस area में काफी मजबूत है

    • Rust में infinite grammar कैसे define की जा सकती है
      उदाहरण के लिए context-free rule S ::= abc|aabbcc|aaabbbccc|... प्रभावी रूप से a^Nb^Nc^N को parse कर सकता है, और यह context-sensitive grammar का एक example है
      example simple है, लेकिन असल में भी ऐसे patterns दिखते हैं; एक example वे languages हैं जो operator definitions की अनुमति देती हैं
      Rust इसे कैसे handle करता है
    • अगर संभव हो तो eBPF disassembler का link share करें
      अच्छा लग रहा है
  • Ragel से parser और lexer लिखने और Go, Java, C++, C इस्तेमाल करने के अनुभव से, अगर कुछ हद तक boilerplate generator हो तो pure C भी लेखक द्वारा बताए Rust code जितना अच्छा है
    simplicity की वजह से शायद बेहतर भी हो
    उदाहरण के लिए JSON parser के लिए जरूरी code ज्यादातर इतना ही है
    https://github.com/gritzko/librdx/blob/master/JSON.lex
    सच तो यह है कि वह eBNF सिर्फ lexer बनाता है, parser वाला हिस्सा भी ज्यादा impressive नहीं है; 120 lines का है और काफी repetitive है
    https://github.com/gritzko/librdx/blob/master/JSON.c
    आखिरकार parser infrastructure उस point तक evolve होता है जहाँ सिर्फ eBNF से parser बनाया जा सके, और मेरे हिसाब से वही saturation point है

    • उस repetitiveness को feature नहीं, drawback माना जा सकता है
      मुझे लगता है Rust के algebraic data types generated syntax tree को handle करना कहीं ज्यादा आसान बना देते हैं
      हालांकि मैं मानता हूँ कि थोड़ा code generation या macro magic C को काफी manageable बना सकता है
    • मुझे Ragel सच में बहुत पसंद है
      लेकिन यहाँ का code
      https://github.com/gritzko/librdx/blob/master/JSON.lex
      क्या [ को valid JSON के रूप में accept नहीं करता
      delimiter = OpenObject | CloseObject | OpenArray | CloseArray | Comma | Colon;
      primitive = Number | String | Literal;
      JSON = ws* ( primitive? ( ws* delimiter ws* primitive? )* ) ws*;
      Root = JSON;
      लगता है JSON में सिर्फ एक delimiter चुना जा सकता है और बाकी सब 0 बार चुना जा सकता है
      आम तौर पर मैं RFC से देखना शुरू करता हूँ
      https://datatracker.ietf.org/doc/html/rfc4627#autoid-3
      यह भी पक्का नहीं कि Ragel से JSON implement किया जा सकता है या नहीं
      जहाँ तक मुझे पता है, Ragel केवल regular languages handle कर सकता है और JSON एक context-free language है
    • Cloudbleed की वजह C/Ragel bug था, और Cloudflare के Rust पर switch करने की वजहों में से भी एक यही था
      https://en.wikipedia.org/wiki/Cloudbleed
  • इस संदर्भ में मुझे Rob Pike का Go lexical scanning वाला talk पसंद है
    शिक्षाप्रद और elegant approach है
    https://www.youtube.com/watch?v=HxaD_trXwRE

    • वह talk शानदार है, लेकिन याद है कि बाद में मैंने एक discussion देखा था कि Go असल में उस तकनीक का इस्तेमाल नहीं करता
      वजह शायद goroutine scheduling overhead या inefficient memory allocation patterns थी
      मुझे मिला सबसे अच्छा discussion [1] है
      efficient lexer और parser बनाने पर एक और बेहतरीन talk Andrew Kelley का “Practical Data Oriented Design” [2] है
      संक्षेप में, यह program की memory usage घटाते हुए उसे cache-friendly बनाकर throughput बढ़ाने की कई strategies बताता है
      1: https://news.ycombinator.com/item?id=31649617
      2: https://www.youtube.com/watch?v=IroPQ150F6c
    • वह talk lexing खुद से ज़्यादा, ऐसे problem में concurrency को express करने के तरीके से जुड़ा लगता है जहाँ concurrency naturally दिमाग में आती है
  • मेरे लिए एक surprising experience था
    high-level compiler parser में इस्तेमाल होने वाली parser combinator library को ज्यों-का-त्यों no-std environment में इस्तेमाल किया, microcontroller के लिए compile किया, और फिर embedded environment में high-performance protocol parser के तौर पर deploy कर सका
    वही library बिल्कुल वैसे ही इस्तेमाल की
    फर्क बस इतना था कि String कम किए और &'static str ज़्यादा इस्तेमाल किए
    इसलिए compiler के साथ खेलना embedded protocol parser बनाने की क्षमता में काफी अच्छी तरह transfer हो जाता है

  • Rust में पूरा AST parser लिखते समय मुश्किल यह थी कि concrete AST types की hierarchy को upcasting और downcasting सहित कैसे represent किया जाए
    तरीका तो मिल गया, लेकिन PhantomData जैसी अजीब type tricks और macros की जरूरत पड़ी
    यहाँ भी काफी ज्यादा macros की जरूरत पड़ी थी, ऐसा लगता है
    संबंधित prior work कैसा दिखता है, यह जानने की उत्सुकता है

    • Rust के algebraic data types और matching syntax अच्छे लगते हैं
      upcasting/downcasting तक पहुँचने से पहले तक
      Rust का पर्याप्त experience नहीं है, इसलिए नहीं पता कि इसे संभालने का कोई अच्छा तरीका है या नहीं
      शायद dynamic traits संभव हों
    • अगर यह open source है, तो क्या कोई public repository है, यह जानना चाहूँगा
  • ऐसे macro code को कैसे debug करते हैं, या codebase में नया आया व्यक्ति इसे कैसे समझता है
    node! macro के use sites और macro definition देखकर भी यह समझना मुश्किल लगता है कि असल में कौन-सा code generate हो रहा है
    क्या examples चलाकर देखना होता है कि कौन-से type hints आते हैं, IDE में hover करने पर expanded version दिख जाता है, या पक्का होने के लिए compiled code refer करना पड़ता है—यह जानना चाहता हूँ
    मैं सिर्फ JS/TS में काम करता हूँ और macros नहीं छूता, इसलिए यह workflow जानने में रुचि है

    • $ cargo expand चलाने पर result code देखा जा सकता है
      Rust असल में कई languages के करीब है, जहाँ “vanilla” Rust, declarative macros, और procedural macros में हर एक की capabilities और dialects थोड़े अलग हैं
      समय के साथ हर एक को संभालने की आदत हो जाती है
      unit tests भी macro changes के असर को समझने के लिए अच्छा प्रयोग-क्षेत्र हैं
    • VSCode आदि में इस्तेमाल होने वाला Rust LSP, rust-analyzer, declarative macros और procedural macros को recursively expand कर सकता है
      बहुत खराब नहीं है, लेकिन codebase में procedural macros जितने कम हों उतना अच्छा
      declarative macros समझने में थोड़े आसान हैं, और maintenance व testing कहीं ज्यादा आसान है
      दूसरी languages की opaque code generation को लेकर भी मेरा अहसास कुछ ऐसा ही है
  • sqlite syntax parsing के लिए शुभकामनाएँ ही देनी पड़ेंगी
    कुछ साल पहले काम के सिलसिले में sqlite के काफी छोटे subset का parser लिखना पड़ा था
    मुझे sqlite सचमुच बहुत पसंद है और वह हमेशा inspiration का source रहा है
    railroad diagrams बेहद उपयोगी हैं
    https://www.sqlite.org/syntaxdiagrams.html
    lemon parser generator को जितनी पहचान मिलनी चाहिए, उतनी नहीं मिली है, ऐसा लगता है
    https://sqlite.org/src/doc/trunk/doc/lemon.html
    language choice के लिहाज से, algebraic data types वाली कोई भी language अच्छी तरह फिट बैठती है
    TypeScript भी इस काम के लिए शानदार हो सकता है
    पहले मैंने Rust में हाथ से parser लिखने पर एक छोटा introductory article भी लिखा था
    https://www.nhatcher.com/post/a-rustic-invitation-to-parsing...