sqleibniz, SQLite lehçesindeki SQL’in sözdizimini, tablo/sütun/fonksiyon varlığını ve çalışma zamanı koşullarını kontrol etmeyi amaçlayan bir statik analiz aracıdır; bu yüzden tokenleştirme ve parsing temel adımlar hâline gelir- Rust’ın
macro_rules!yapısı, AST düğümü struct’larını,Nodetrait implementasyonlarını ve Go tarzı tablo tabanlı testleri tekrar olmadan oluşturmaya yardımcı olur matches!vematchdesenleri kullanıldığında SQLite sayı literalleri, tanımlayıcılar, semboller veEXPLAIN QUERY PLANgibi gramer dallanmaları koda yakın biçimde aktarılabilirOption’ınis_some_and,map,map_ormetodları ve?operatörü, girdi ve token akışı işlemede değer varlığını, dönüşümü, varsayılan değerleri ve hata yayılımını kısa ve net hâle getirir- Rust iterator’ları sayı literallerinden
_kaldırma, blob içindeki onaltılık karakterleri doğrulama ve hata konumu hesaplama için kullanılır; bu da tokenleştirme ve parsing kodunu daha okunabilir kılar
sqleibnizin analiz akışı
sqleibniz, SQLite lehçesini hedefleyerek geliştirilmekte olan bir SQL analiz aracıdır- SQL girdisi üzerinde sözdizimi denetimi, tablo/sütun/fonksiyon varlığı kontrolü ve yerleşik SQLite çalışma zamanı ile birlikte koşul doğrulaması yapmayı amaçlar
- Hata mesajlarının bağlam ve açıklama sunması, ayrıca belirli tanıların yok sayılabilmesine olanak vermesi hedeflenir
- Analiz akışı sözcüksel analiz/tokenleştirme ile başlar; SQLite belgelerine göre SQL parsing’i ve sonuç yapısının analiziyle devam eder
- Statik analiz kısmı tamamlandıktan sonra SQL için bir LSP sunucusu yazılması da planlanmaktadır
Makrolarla AST düğümlerindeki tekrarı kaldırma
sqleibnizin AST düğümleriTokeniçeren struct’lardır ve tüm düğümlerinNodetrait’ini implemente etmesi gerekirNodetrait’istd::fmt::Debug’ı süper trait olarak kullanır; böylece yalnızcaDebug’ı sağlayan tiplerNode’u implemente edebilir- Her düğüm için struct tanımını ve
fn token(&self) -> &Tokenimplementasyonunu tekrar etmemek adınanode!makrosu bunları üretir- Düğüm adı
identmetavariable olarak alınır - Belge dizgesi
literalmetavariable olarak alınır - Ek alanlar
$($field_name:ident:$field_type:ty),*biçiminde tekrar edilerek işlenir
- Düğüm adı
Literaldüğümü yalnızca token alanına sahiptir;Explaindüğümü ise ek olarakchild: Option<Box<dyn Node>>alanını taşır- Dokümantasyon yorumları,
///yerine#[doc = $documentation]biçiminde makro argümanı olarak derleyiciye aktarılır
Go tarzı tablo tabanlı testleri Rust makrolarıyla uygulama
- Go’daki tablo tabanlı testlerde olduğu gibi, girdi case’leri dizisi üzerinde dönüp her case’in bağımsız test olarak çalıştırıldığı yapı Rust makrolarıyla yeniden oluşturulur
- Lexer testleri
test_group_pass_assert!vetest_group_fail!makrolarını kullanır- Başarılı testlerde girdi
Lexer’a verilir veLexer.run()sonucundaki token tipi listesi beklenen değerle karşılaştırılır - Başarısız testlerde sonuç token vektörünün boş olduğu ve
Lexer.errorsiçinde en az bir hata bulunduğu kontrol edilir cargo testçalıştırıldığında her case ayrı bir test fonksiyonu gibiokveyafailgeri bildirimi üretir
- Başarılı testlerde girdi
- Parser testleri de aynı yapıyı izler; ancak lexer çalıştıktan sonra
Parserbaşlatılır veparse()sonucu denetlenirEXPLAIN VACUUM;veEXPLAIN QUERY PLAN VACUUM;başarılı case’lerdirEXPLAIN;veEXPLAIN QUERY PLAN;başarısız case’lerdir- Başarısız case’ler, SQLite
sql-stmtgramerine göreEXPLAINsonrasında bir ifade gerektiği koşulunu doğrular
macro_rules!ın rahatsız edici yanları
macro_rules!içinderust-analyzerdesteği sınırlıdır- Gerçek IntelliSense yoktur
- Tanıma gitme yoktur
- Literaller ve dil yapısı imzaları için hover yoktur
cargo fmt,macro_rules!içini ve makro çağrı yerlerini biçimlendirmez veya girintilemeztreesittervechroma,macro_rules!sözdizimi renklendirmesinde zaman zaman zorlanır- Makro dokümantasyonu da görece yetersizdir
Karakter eşleştirmede öne çıkan matches! ve match
- Lexer’daki karakter karşılaştırmaları diğer işlemlerin temelini oluşturur; Rust’ın
matches!makrosu vematchdesenleri bu bölümü kısa ve anlaşılır kılar - SQLite sayı tespiti
matches!ile yazılır+,-_.a..=f,A..=F0..=9
- Tanımlayıcı tespiti de
matches!(c, 'a'..='z' | 'A'..='Z' | '_' | '0'..='9')biçiminde ifade edilir - Lexer’ın ana döngüsü mevcut karakteri
matchile dallandırır- Boşluk karakterleri atlanır
*,;,,,%gibi karakterler karşılık gelen token’ı üretir- Bilinmeyen sembol işleme örneği atlanmış ve
panic!("whoops")ile gösterilmiştir
Token eşleştirme ile SQL gramer yapısını işleme
- Lexer, karakter akışını konum bilgisi ve tip bilgisi taşıyan
Tokenstruct akışına dönüştürür; parser ise bunu tüketerek AST oluşturur Typeenum’uKeyword,Ident,Number,String,Blob,Boolean,ParamName,Param,Dot,Asteriks,Semicolon,Percent,Comma,Eofgibi değerleri içerirsql_stmt_prefix, SQLite belgelerindekiEXPLAINifadesini işleyen parser fonksiyonudur- Mevcut token
Type::Keyword(Keyword::EXPLAIN)ise birExplaindüğümü oluşturur veEXPLAINtoken’ını tüketir - Sonraki token
QUERYiseQUERYvePLANart arda tüketilir - Ardından gerçek SQL ifadesi
childolarak parse edilir EXPLAINdeğilse normalsql_stmtişleme çağrılır
- Mevcut token
literal_value, string, sayı, blob ve boolean ile birlikteNULL,CURRENT_TIME,CURRENT_DATE,CURRENT_TIMESTAMPgibi anahtar sözcük literalleriniLiteraldüğümüne dönüştürür
Hata gösterimi ve Option kullanımı
- Lexer ve parser, SQL ifadesi sonundaki noktalı virgül eksikliği gibi durumları kullanıcıya hata olarak gösterir
- Rust’ın
?operatörü hata işleme ve yayma için kullanılır Option::is_some_and, sonraki karakterin veya mevcut karakterin var olup koşulu sağladığını kontrol ederken kullanılırself.source.get(self.pos + 1).is_some_and(...)self.source.get(self.pos).is_some_and(...)
Option::map,Vec<u8>girdisinde sonraki baytıchar’a çevirmek için kullanılırOption::map_or, mevcut token veya sonraki token varsa yalnızca o durumda tip karşılaştırması yapıp yoksafalsedöndürecek şekilde kullanılır
Iterator’larla sayı ve blob işleme
- SQLite sayı parsing’i
_kullanımına izin verir; ancak Rust sayı parsing’i_kabul etmediği için lexer,_karakterlerini de tüketir ve parsing öncesinde kaldırır - Bu işlem bir iterator zinciriyle yazılır
- Bayt dilimi alınır
- Her bayt
char’a dönüştürülür _olmayan karakterler filtrelenirStringolarak toplanır
- Bu durumda
unwrap_or_default()kullanılır; ancak boş string sayı olarak geçerli olmadığından parser zaten başarısız olur - Go’da aynı işlem için karakter listesini dolaşmak,
strings.Builder’a bayt yazmak ve ardından yeniden string oluşturmak gerekir - SQLite blob,
x'<hex>'biçiminde onaltılık veriye izin verdiğinden, string’deki her karakterchars().enumerate()ile dolaşılarakis_ascii_hexdigit()olup olmadığı kontrol edilirenumerate, hatalı karakterin konum bilgisini hata gösterimi için elde etmekte kullanılır- Geçersiz onaltılık karakterle karşılaşılırsa hata oluşturulur ve işlem durdurulur
1 yorum
Hacker News yorumları
İki ay önceye kadar ben de yazarla aynı düşünüyordum ama Rust’ın katı sınırı olan borrow checker ile sürekli çarpıştım
Cebirsel veri türleri, örneğin enum ve pattern matching, gerçekten harikaydı ama borrow checker ve düşük seviyeli bellek kaygıları yüzünden projenin asıl konusu olan programlama dili sorunlarından çok borrow checker ile boğuşmaya zaman harcadım
Bu yüzden tokenization ve parsing idare ederdi ama interpreter ve type checking acı verici hale geldi; daha uygun bir dil ararken F#, Zig/C ve Go’yu değerlendirdikten sonra OCaml’i keşfettim
Sözdizimi daha dostça bir Haskell gibi ve lifetimesız Rust gibi göründüğü için ikna oldum; ilk Rust derleyicisi de OCaml ile yazılmıştı ve programlama dilleri alanında iyi biliniyor
Hâlâ öğreniyorum, bu yüzden adil bir değerlendirme yapmak zor ama şu ana kadar tam olarak aradığım şeye en yakın olan bu
Pratik ve hızlı, gerçekten düşük seviye değil, derlemesi de hızlı ve hepsinden önemlisi çok popüler olduğu için tüm kütüphaneler var; o yüzden kullanmam gerekiyormuş gibi geliyor
Ama dilin kendisinden neredeyse irrasyonel biçimde nefret ediyorum ve her yönüyle çirkin geliyor
2009’da C tarafındaki insanlar tarafından yapılmış bir dil; o tarihin ölçütlerine göre bile son 20 yılda programlama dili tasarımında ilginç olan şeylerden habersizmiş gibi duruyor
2009’daki PHP bile Go’dan daha modern ve daha iyi tasarlanmış bir dildi ve Go’nun o zamandan beri de kayda değer biçimde ilerlemediği hissinden kurtulamıyorum
Bunun yerine cheap clone ve interning yapabilen, örneğin bir static string kütüphanesi gibi yapılar kullanmak; metin konumları için de yalnızca indeks tutmak daha iyi
Mümkünse referans saklamaktan kesinlikle kaçınmak gerekir
Cheap/free clone yapılabilen şeylerden daha fazlasını tuttukça borrow checker ile daha az kavga edersiniz ve gerekirse clone’a geçebilirsiniz
Asıl interpreter tarafında ise arena benzeri yaklaşımlarla bellek yönetimine yardımcı olan kütüphaneler epey faydalı
Çok uzmanlaşmış bir alan ama hem performans hem kullanılabilirlik sağlıyor ve Ruffle gibi projeler de bu deseni sık kullanıyor
Yine de OCaml ve Haskell, yerleşik reference counting ve garbage collection sayesinde bunları “bedavaya” sağlıyor; buna rağmen Rust ile çok hızlı gitme fikri yine de hoşuma gidiyor
Go, modernleştirilmiş C’ye daha yakın ve sunduğu model çok basit
C#’tan gelince bu sadelik yüzünden öğrenmesi tersine daha zordu; kavramsal yükünün düşük olması avantajı ve ayrıntıcılık ödününü kabul edebileceğiniz küçük, odaklı uygulamalara iyi uyuyor
Tavsiye edecek olsam F# derdim; modern C# da bence fena değil
Microsoft işin içinde diye tamamen şeytani büyük şirketlerin yaptığı hiçbir şeyi kullanmayacağınız bir dünyada yaşamak isterseniz işiniz zorlaşır
Java, Go, Python, TypeScript/JavaScript ve Swift de buna giriyor; o zaman da neredeyse hiç seçenek kalmıyor
Yaklaşık bir yıl OCaml kullandıktan sonra ne düşündüğünü merak ediyorum
Haskell türevi diller ilginç ama Haskell’in kendisinde öğrenme eğrisi karşılığında aldığım fayda benim için iyi değildi; Rust da benzer
C#’ta tür sistemini derinlemesine kurcalayıp ustalaştım ama Rust’ta o kadar derine inmeye vaktim yok
Elbette ikili dosya boyutuna ve bellek kullanımına 10–20MB kadar ekliyor ama günümüz ölçülerinde bu neredeyse hiçbir şey
Örneğin Tailscale, mobil ve masaüstü uygulamalarında çapraz platform WireGuard katmanı olarak Go kullanıyor gibi görünüyor ve iyi çalışıyor gibi
Go ile native UI yazmazdım ama düşük seviyeli işler için mükemmel
TinyGo, mikrodenetleyiciler veya WebAssembly için Go yazmayı da mümkün kılıyor; desteklenmeyen çok şey var ama standart kütüphanenin önemli bir kısmı kullanılabiliyor
Örneğin Go derleyicisinin kendisi de Go ile yazılmış
Cross-compilation ve görece küçük binary’ler sayesinde dağıtım çok kolay
Ama sözdizimsel şekerin az olduğu doğru ve işlevsel tarzdaki pattern matching için pek uygun değil
Parsing’e yaklaşım olarak biraz tuhaf görünüyor ve yazarın Rust’a ve onun temelindeki programlama dili kavramlarına çok da aşina olmadığı izlenimini veriyor
Birkaç noktaya bakarsak, AST cebirsel veri türleriyle tanımlansa çok daha basit olurdu gibi geliyor
sqlite sözdiziminin aniden yeni düğümler ekleyerek karmaşık bir encoding gerektirecek şekilde genişlemesi pek olası görünmüyor
Mevcut encoding, nesne yönelimliye alışkın ama cebirsel veri türlerine alışkın olmayan birinin aklına gelecek türden görünüyor
“Makrolar çoğu dilde farklı çalışır ama ana sebep kod tekrarını azaltmak ve yinelemeyi düşürmektir” sözü, fonksiyonlar gibi tüm soyutlama mekanizmaları için de söylenebilir
Makroları tanımlayan ayırt edici özellik, compile time’da çalışmalarıdır
Parser’ı temiz biçimde yapılandırmanın yollarını görmek için parser combinator araştırmaları iyi bir başlangıç noktası olabilir
Blog başlığı da “Why I love ...”; işaret ettiğiniz noktalar geçerli görünse de deneyim eksikliğini özellikle vurgulamak çok gerekli görünmüyor
Birinin programlamayı sevmesi iyi bir şey ve deneyim zamanla gelecektir
Bunu fonksiyonlarla yapamazsınız
Forsyth-Edwards satranç notasyonu için küçük bir ayrıştırıcı [0] yazmış biri olarak, sadelik ve okunabilirlik açısından Haskell'in ezici biçimde üstün olduğunu düşünüyorum
Neredeyse BNF gibi okunuyor ve teknik törensellik neredeyse hiç yok; bu yüzden gerçekten ayrıştırmak istediğiniz dilbilgisine odaklanabiliyorsunuz
[0] https://github.com/ryandv/chesskell/blob/master/src/Chess/Fa...
[1] https://en.wikipedia.org/wiki/Forsyth%E2%80%93Edwards_Notati...
Rust'ta benzer bir yaklaşımın mümkün olmaması için açık bir neden olup olmadığını merak ediyorum
Örneğin winnow [1] yeterince bildirime dayalı bir stil sunuyor gibi görünüyor ve Rust'ta başka birçok parser combinator kütüphanesi de var
[1]: https://docs.rs/winnow/latest/winnow/
Çünkü tek döngülü basit bir işleve indirgenebiliyor
Birkaç gün önce deneysel bir quad-bitboard uygulaması için bir FEN “parser”ı yazdım ve neredeyse kendi kendine yazıldı
Bu arada Hackage'daki chessIO'nun yazarıyım
Rust ile bir eBPF disassembler ve yarım kalmış bir emülatör yazdım; ayrıştırma türü işler yapmak için Rust oldukça keyifli bir dildi
Ama yazarın vaka çalışmasının altıda birine bile gelmeden makrolara ihtiyaç duymaya başlaması, sanki kendi argümanını zayıflatıyor gibi
Makrolar tam anlamıyla kod üretimi değil, ama dil içinde işi deyimsel biçimde yaptığınız hissini de çok güçlü vermiyor
Bunu kötülemek için söylemiyorum; Rust'ın burada gerçekten oldukça güçlü olduğunu düşünüyorum
Örneğin bağlamdan bağımsız kural
S ::= abc|aabbcc|aaabbbccc|...,a^Nb^Nc^Nifadesini etkin biçimde ayrıştırabiliyor ve bu da bağlama duyarlı dilbilgisine bir örnekBu basit bir örnek ama pratikte de benzer şeyler görülebiliyor; bir dilin operatör tanımlarına izin vermesi buna bir örnek
Rust bunu nasıl ele alıyor?
Kulağa hoş geliyor
Ragel ile parser ve lexer yazmış, ayrıca Go, Java, C++, C kullanmış biri olarak, belli bir düzeyde boilerplate üretici elinizde varsa saf C bile yazarın anlattığı Rust kodu kadar iyi olabiliyor
Sadelik yüzünden hatta daha iyi bile olabilir
Örneğin bir JSON parser için gereken kodun büyük kısmı aşağı yukarı şu kadar
https://github.com/gritzko/librdx/blob/master/JSON.lex
Aslında o eBNF sadece lexer'ı üretiyor ve parser kısmı da pek etkileyici değil; 120 satır ve epey tekrarlı
https://github.com/gritzko/librdx/blob/master/JSON.c
Sonuçta parser altyapısı, yalnızca eBNF ile parser üretebildiğiniz noktaya kadar evriliyor ve bence doyum noktası da bu
Rust'ın cebirsel veri tipleri, üretilen sözdizimi ağaçlarını ele almayı çok daha kolay hale getiriyor diye düşünüyorum
Yine de biraz kod üretimi ya da makro sihriyle C'nin oldukça kullanışlı hale gelebileceğine katılıyorum
Ama burada kod
https://github.com/gritzko/librdx/blob/master/JSON.lex
[karakterini geçerli JSON olarak kabul etmiyor mu?delimiter = OpenObject | CloseObject | OpenArray | CloseArray | Comma | Colon;primitive = Number | String | Literal;JSON = ws* ( primitive? ( ws* delimiter ws* primitive? )* ) ws*;Root = JSON;JSON'da tek bir delimiter seçip geri kalan her şeyi 0 kez seçmek mümkün gibi görünüyor
Ben genelde RFC ile başlarım
https://datatracker.ietf.org/doc/html/rfc4627#autoid-3
JSON'un Ragel ile uygulanıp uygulanamayacağından da emin değilim
Bildiğim kadarıyla Ragel yalnızca düzenli dilleri işleyebiliyor, JSON ise bağlamdan bağımsız bir dil
https://en.wikipedia.org/wiki/Cloudbleed
Bununla ilgili olarak Rob Pike’ın Go üzerine yaptığı leksik tarama konuşmasını seviyorum
eğitici ve zarif bir yaklaşım
https://www.youtube.com/watch?v=HxaD_trXwRE
nedeni goroutine zamanlama ek yükü ya da verimsiz bellek ayırma kalıpları gibi bir şeydi sanırım
bulabildiğim en iyi tartışma [1]
verimli lexer ve parser yazımı üzerine bir diğer harika konuşma da Andrew Kelley’nin “Practical Data Oriented Design” [2] sunumu
özetle, programın bellek kullanımını azaltırken onu önbellek dostu hale getirip iş hacmini artıran çeşitli stratejileri anlatıyor
1: https://news.ycombinator.com/item?id=31649617
2: https://www.youtube.com/watch?v=IroPQ150F6c
Benim için şaşırtıcı bir deneyim vardı
üst düzey bir derleyici parser’ında kullandığım parser combinator kütüphanesini aynen no-std ortamında kullanabildim, mikrodenetleyici için derledim ve gömülü ortamda yüksek performanslı bir protokol parser’ı olarak dağıtabildim
aynı kütüphaneyi olduğu gibi kullandım
fark, String kullanımını azaltıp daha fazla
&'static strkullanmak kadardıbu yüzden derleyicilerle uğraşmak, gömülü protokol parser’ları yazma becerisine oldukça iyi aktarılıyor
Rust ile tüm AST parser’ını yazarken zorlandığım nokta, somut AST tiplerinin hiyerarşisini upcasting ve downcasting dahil ifade etmekti
bir yolunu buldum ama
PhantomDatagibi tuhaf tip oyunları ve makrolar gerektiburada da epey ağır makro kullanımı gerekmiş gibi görünüyor
bu konuda önceki çalışmaların nasıl göründüğünü merak ediyorum
tabii upcasting/downcasting noktasına gelene kadar
Rust deneyimim yeterli değil, bunun için iyi bir yaklaşım var mı bilmiyorum
belki dynamic trait’ler işe yarıyordur
Bu tür makro kodları nasıl debug ediyorsunuz ya da kod tabanına yeni gelen biri bunu nasıl anlıyor
node!makrosunun kullanım yerlerine ve makro tanımına baksam bile gerçekte hangi kodun üretildiğini anlamanın zor olacağını düşünüyorumörnekleri çalıştırıp hangi type hint’lerin çıktığına bakmak mı gerekiyor, IDE’de üstüne gelince genişletilmiş sürümü görüyor musunuz, yoksa emin olmak için derlenmiş koda mı bakmak gerekiyor merak ediyorum
Sadece JS/TS ile çalıştığım için makrolarla uğraşmıyorum; o yüzden bu iş akışını merak ediyorum
$ cargo expandçalıştırırsanız ortaya çıkan kodu görebilirsinizRust aslında birkaç dile daha yakın; “vanilla” Rust, declarative macro’lar ve procedural macro’ların her birinin biraz farklı yetenekleri ve lehçeleri var
zamanla her biriyle çalışmaya alışıyorsunuz
unit test’ler de makro değişikliklerinin etkisini anlamak için iyi bir deney alanı oluyor
çok kötü değil, ama bir kod tabanında procedural macro ne kadar azsa o kadar iyi
declarative macro’lar biraz daha anlaşılır oluyor, bakım ve test de çok daha kolay
diğer dillerdeki opak kod üretimi için de benzer hissediyorum
sqlite sözdizimini parse etmek için şimdiden bol şans
birkaç yıl önce iş için sqlite’ın oldukça küçük bir alt kümesinin parser’ını yazmam gerekmişti
sqlite’ı gerçekten çok seviyorum ve her zaman ilham kaynağı olmuştur
demiryolu diyagramları inanılmaz faydalı
https://www.sqlite.org/syntaxdiagrams.html
lemon parser generator’ın yeterince takdir edilmediğini düşünüyorum
https://sqlite.org/src/doc/trunk/doc/lemon.html
dil seçimi açısından, cebirsel veri tipleri olan herhangi bir dil bunun için iyi iş çıkarır
TypeScript bile bu amaç için harika olabilir
bir zamanlar Rust ile elle parser yazmaya giriş niteliğinde küçük bir yazı da yazmıştım
https://www.nhatcher.com/post/a-rustic-invitation-to-parsing...