sqleibnizSQLite dialect SQL की syntax, table·column·function की मौजूदगी और runtime conditions की जांच करने वाला static analysis tool है, और इसके लिए tokenization और parsing मुख्य चरण हैं- Rust का
macro_rules!AST node structs,Nodetrait implementations, और Go-style table-based tests को बिना repetition के बनाने में मदद करता है matches!औरmatchpatterns का उपयोग करने से 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
sqleibnizSQLite 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 कोNodetrait implement करना होता हैNodetraitstd::fmt::Debugको supertrait के रूप में उपयोग करता है, इसलिए केवलDebugsatisfy करने वाले types हीNodeimplement कर सकते हैं- हर node के लिए struct definition और
fn token(&self) -> &Tokenimplementation दोहराने से बचने के लिएnode!macro इन्हें generate करता है- Node name
identmetavariable से लिया जाता है - Documentation string
literalmetavariable से लिया जाता है - Additional fields को
$($field_name:ident:$field_type:ty),*form में repeat-process किया जाता है
- Node name
Literalnode में केवल token field होता है, औरExplainnode में अतिरिक्त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याfailfeedback देता है
- Passing tests input को
- Parser tests भी वही structure follow करते हैं, लेकिन lexer run के बाद
Parserinitialize करते हैं औरparse()result check करते हैंEXPLAIN VACUUM;औरEXPLAIN QUERY PLAN VACUUM;passing cases हैंEXPLAIN;औरEXPLAIN QUERY PLAN;failing cases हैं- Failing cases SQLite
sql-stmtgrammar मेंEXPLAINके बाद statement आवश्यक होने की condition verify करते हैं
macro_rules! की असुविधाएं
macro_rules!के अंदरrust-analyzersupport limited है- वास्तविक IntelliSense नहीं है
- Go to definition नहीं है
- Literals और language structure signatures पर hover नहीं है
cargo fmtmacro_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 वmatchpatterns इस हिस्से को concise बनाते हैं - SQLite number detection
matches!से लिखा गया है+,-_.a..=f,A..=F0..=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 वाले
Tokenstruct stream में बदलता है, और parser उसे consume करके AST बनाता है Typeenum मेंKeyword,Ident,Number,String,Blob,Boolean,ParamName,Param,Dot,Asteriks,Semicolon,Percent,Comma,Eofआदि शामिल हैंsql_stmt_prefixSQLite documentation केEXPLAINstatement को handle करने वाला parser function है- अगर current token
Type::Keyword(Keyword::EXPLAIN)है, तोExplainnode बनाता है औरEXPLAINconsume करता है - अगर next token
QUERYहै, तोQUERYऔरPLANको consecutively consume करता है - इसके बाद actual SQL statement को
childके रूप में parse करता है - अगर
EXPLAINनहीं है, तो generalsql_stmthandling call करता है
- अगर current token
literal_valuestrings, numbers, blob, boolean के साथNULL,CURRENT_TIME,CURRENT_DATE,CURRENT_TIMESTAMPजैसे keyword literals कोLiteralnode बनाता है
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::mapVec<u8>input में next byte कोcharमें बदलने के लिए इस्तेमाल होता हैOption::map_orcurrent token या next token होने पर ही type comparison करने और न होने परfalsereturn करने के तरीके से इस्तेमाल होता है
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 किया जाता हैenumerateinvalid character की location information error display के लिए पाने में इस्तेमाल होता है- Invalid hexadecimal character मिलने पर error create करके processing रोक दी जाती है
1 टिप्पणियां
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 के क्षेत्र में यह जाना-पहचाना है
अभी सीख ही रहा हूँ, इसलिए निष्पक्ष मूल्यांकन मुश्किल है, लेकिन अब तक यह ठीक उसी चीज़ के करीब है जिसे मैं खोज रहा था
यह practical और तेज़ है, सचमुच low-level नहीं है, compile भी तेज़ होता है, और सबसे बढ़कर लोकप्रिय है इसलिए libraries भी सब हैं, तो लगता है कि इसे इस्तेमाल करना चाहिए
लेकिन भाषा खुद मुझे लगभग अतार्किक रूप से नापसंद है, और इसकी हर चीज़ बदसूरत लगती है
यह 2009 में C वाले लोगों द्वारा बनाई गई भाषा है, लेकिन उस समय के हिसाब से भी ऐसा लगता है जैसे उन्हें पिछले 20 वर्षों में programming language design में हुई दिलचस्प चीज़ों का पता ही न हो
2009 का PHP भी Go से ज़्यादा modern और बेहतर designed language था, और यह एहसास हटता नहीं कि Go उसके बाद भी खास बेहतर नहीं हुआ
इसके बजाय 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 से बहुत तेज़ जाने का विचार मुझे पसंद है
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 में उतनी गहराई तक जाने का समय नहीं है
बेशक यह 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 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 हो सकता है
blog का title भी “Why I love ...” है, और आलोचना अपने-आप में सही लगती है, लेकिन अनुभव की कमी बताना ज़रूरी नहीं लगता
किसी का programming पसंद करना अच्छी बात है, और अनुभव समय के साथ आ जाएगा
यह 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...
सोच रहा हूँ कि Rust में वैसा approach न हो पाने की कोई स्पष्ट वजह है क्या
उदाहरण के लिए winnow [1] काफी declarative style देता दिखता है, और Rust में कई अन्य parser combinator libraries भी हैं
[1]: https://docs.rs/winnow/latest/winnow/
क्योंकि इसे एक 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 में काफी मजबूत है
उदाहरण के लिए 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 करता है
अच्छा लग रहा है
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 है
मुझे लगता है Rust के algebraic data types generated syntax tree को handle करना कहीं ज्यादा आसान बना देते हैं
हालांकि मैं मानता हूँ कि थोड़ा code generation या macro magic C को काफी manageable बना सकता है
लेकिन यहाँ का 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 है
https://en.wikipedia.org/wiki/Cloudbleed
इस संदर्भ में मुझे Rob Pike का Go lexical scanning वाला talk पसंद है
शिक्षाप्रद और elegant approach है
https://www.youtube.com/watch?v=HxaD_trXwRE
वजह शायद 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
मेरे लिए एक 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 कैसा दिखता है, यह जानने की उत्सुकता है
upcasting/downcasting तक पहुँचने से पहले तक
Rust का पर्याप्त experience नहीं है, इसलिए नहीं पता कि इसे संभालने का कोई अच्छा तरीका है या नहीं
शायद dynamic traits संभव हों
ऐसे 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 के असर को समझने के लिए अच्छा प्रयोग-क्षेत्र हैं
बहुत खराब नहीं है, लेकिन 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...