simple calculator language design via using antlr v3
Ufak işler için ufak programlama dillerine ihtiyac duyabiliriz. Örneğin matematiksel ifadeleri resim'e dönüştüren bir dil olabilir ya da matris hesaplamaları yapan bir dil ya da belirlediğimiz özel bir metin biçeminde sorgu yapan bir dil ya da oyuncak arabamızı kontrol eden bir dil ya da siteniz için tema motoru.
Javy'de kullandığımız antlr çözümleyici oluşturucu (parser generator) ve jline konsol paketleri ile ufak bir hesap makinesi dili yapmaya çalışalım. Bu takdirde Javy'nin temel çalışma mantığı da göstermiş olacağım.
Bir betik veya uçbirim (konsol) girdisi, derleyici veya yorumlayıcı tarafında iki aşamada çözümlenir. Birinci aşama sözcük çözümleme (lexing) ikinci aşama ise sözdizim çözümlemedir (parsing). Bu çözümleme çeşitli kurallara göre yapılmalıdır. Ve bu kuralları oluşturacağımız da bir betimleme dili olmalıdır. İşte bu betimleme dili BNF'tir.
Oluşturacağımız lexer ve parser kural dosyası şu şekildedir.
grammar HesapMakinesi;
options {
output = AST;
ASTLabelType = Tree;
}
// ### SOZ DIZIM COZUMLEYICI - AYRISTIRICI (PARSER) KURALLARI
basla : (islem (SATIR_SONU islem)* SATIR_SONU?)? ;
// basla kurali icin gerekli olanlar
// * Bos bir ifadeye imkan taninabilmeli. Hicbir veri girilmese de
// soz dizim dogru olmalidir.
// * Islem Betigin son islemi ise satir sonu gelmeyebilir. Bunun haricinde
// her islemden sonra satir sonu gelmelidir.
// * Iki bagimsiz islem arada islec (operator) olmadan asla arka arkaya
// gelmemelidir.
islem : oncelikli_islem ((ARTI^ | EKSI^) oncelikli_islem)* ;
oncelikli_islem : SAYI ((CARPI^ | BOLU^) SAYI)* ;
// ### SOZCUK COZUMLEYICI (LEXER) KURALLARI
SATIR_SONU : ('\u000D' | '\u000A') ;
SAYI : RAKAM+ ;
ARTI : '+' ;
EKSI : '-' ;
CARPI : '*' ;
BOLU : '/' ;
BEYAZ_BOSLUKLAR : (' ' | '\t')+ {$channel = HIDDEN;} ;
// ### SOZCUK COZUMLEYICI ALT PARCALARI
fragment
RAKAM : '0' .. '9' ;
BNF ile DFA (Deterministic Finite Automata - Belirgin Sonlu Otomat) oluşturabiliriz. Önce sözcük çözümleyicisi (lexer) için BNF kuralları oluşturalım.
ARTI : '+';
ARTI adlı bir sözcüğümüz (token) oldu. Bu tarayıcı sözcüğüne (token) atanan karakter ise '+'dır. Benzer şekilde diğer işleçlerimizi (operatör) de yazalım.
EKSI : '-';
CARPI : '*';
BLU : '/';
Bunlara ilaveten satır sonlarını gösteren de bir kural eklememiz gerekir.
SATIR_SONU : ('\u000D' | '\u000A') ;
SATIR_SONU sözcüğü ya 0D ya da 0A onaltılık değerlere sahip olacaktır. BNF dilinde anlaşıldığı gibi '|' düşey boru işareti yada (OR) manasına gelir.
Yazdığımız bu dilde işe yaramayan boşluk karakterlerini ise aşağıdaki kural ile tanımlayalım.
BEYAZ_BOSLUKLAR : (' ' | '\t')+ ;
DFA'nın bir başka şekli olan Düzenli ifadeler (regular expressions) ile uğraşmış olan geliştiriciler bilirler. + işleci birden fazla tekrar manasına gelir * ise 0'dan fazla tekrar manasına gelir. Örnek kural üzerinde gösterelim.
SAAT : 's' 'a'+ 't'
Yukarıdaki kural 'sat', 'saat', 'saat', 'saaaaaaaaaat' ifadeleri ile eşleşecektir. Fakat 'st' ile eşleşmeyecektir. Çünkü + işleci 1 ve 1 den fazla tekrar manasına gelir. * işleci ise 0 veya daha fazla tekrar manasına geleceğinden 'st' ile de eşleşebilir.
SAAT : 's' 'a'* 't'
Yukarıdaki kural, 'st', 'sat', 'saat', 'saaaaaaat' gibi ifadeler ile eşleşir. Benzer şekilde BEYAZ_BOSLUKLAR sözcüğü (token) 'sekme', 'boşluk', 'sekme-boşluk', 'bosluk-sekme', 'sekme-sekme', 'sekme-sekme-sekme', 'boşluk-boşluk-sekme' gibi daha birçok eşleşmeye sahiptir. Yani betiğimizdeki tüm boşluk ifadelerini kapsar.
RAKAM : '0' .. '9';
RAKAM sözcüğü 0'dan 9'a kadar tüm rakamlar ile eşleşir. Yukarıda aralık belirten '..' işleci aslında birçok veya işleminin kısa yoludur. Yukarıdaki kural şu şekilde de yazılabilirdi.
RAKAM : '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
Tüm kurallara ilaveten son sözcük çözümleyici (lexer) kuralımız ise şu şekilde olmalıdır.
SAYI : RAKAM+;
SAYI sözcüğü (token), bir veya birden fazla rakamın birleşmesinden oluşacaktır. '1', '12', '1000', '0122' gibi bir çok eşleşme SAYI sözcüğü ile ifade edilebilir.
Örnek hesaplama ifademiz:
1 + 56 - 31 / 89 * 21
Bu ifadeyi söcük çözümleyici şu şekilde çözümler.
SAYI ARTI SAYI EKSI SAYI BOLU SAYI CARPI SAYI
Tabi şu anda yaptığımız ayrıştırıcı gerekli sözcükleri çözümlüyor. Fakat sözdizim konusunda sorunları bulunmakta örneğin:
++4423412+++++++++++3444++--++ 14324 142314
İfadesi de benzer şekilde ayrıştırılacaktır. Bu bağlamda söz dizim kurallarımız olması gerekir bu işlemi ise parser dediğimiz söz dizim çözümleyicisi yapacaktır. Bu kurallar sayesinde işleç öncelikleri de belirleyebiliriz.
Öncelikle bir kök kuralımız olması gerekmektedir. Bu kök kural şunları gözetmeli:
* Boş bir ifadeye imkan tanınabilmeli. Hiçbir veri girilmese de söz dizim doğru olmalı
* İşlem Betiğin son işlemi ise satır sonu gelmeyebilir. Bunun haricinde her işlemden sonra satır sonu gelmelidir.
* İki bağımsız işlem arada operatör olmadan asla arka arkaya gelmemelidir.
Bu veriler ışığın da kuralımız şu şekilde olabilir.
basla : (islem (SATIR_SONU islem)* SATIR_SONU?)? ;

Kuralın DFA grafiği yukarıdaki şekilde olabilir. Kural direkt geçilebiliyor bu da demek oluyor ki boş bir ifade de kabul edilebilir. Ve bir işlem'den sonra SATIR_SONU gelebilir veya gelmeyebilir ama iki işlemden sonra arada mutlaka SATIR_SONU olmalıdır. Fakat son işlemden sonra her zaman SATIR_SONU ister konulabilir ister konulmayabilir.
Yukarıdaki kurallardan sonra işlem kuralını açalım. İşlem kuralında önce bir toplama - çıkarma işlemi yapıyoruz. Böylelikle işleç önceliğini çarpma ve bölmeye vermiş oluruz.
Kural şu şekilde olmalıdır.
islem : oncelikli_islem ((ARTI | EKSI) oncelikli_islem)*;
Bir öncelikli işlemden sonra başka bir veya birden fazla öncelikli işlem geliyor ise mutlaka ARTI veya EKSI ile birleştirilmelidir.

oncelikli_islem kuralımız ise buna benzer bir ifade olacaktır.
oncelikli_islem : SAYI ((CARPI | BOLU) SAYI)*;
Bir sayı tek veya birden çok çarpı/bölü işleçleri ile birleştirilmiş olmalıdır.

Şimdi bir örnek üzerinde kuralların kullanımınından bahsedelim.
1 + 15 / 5 + 1 * 1
Yukarıdaki ifadeyi sözdizimin kök kuralı olarak irdeleyelim öncelikle.
islem
İfade hiçbir SATIR_SONU ile ayrılmamış tek bir işlemdir. 'islem' kuralı nazarından bakarsak şöyle çözümlenecektir.
oncelikli_islem ARTI oncelikli_islem ARTI oncelikli_islem
Görüldüğü gibi islem ifadesi 3 farklı kola ayrıldı.
1. işlem : 1
2. işlem : 15 / 5
3. işlem : 1 * 1
Bu sözdizim kurallarının yanında bir de ağaç yapısı oluşturmuş olduk. Ağaç yapısı şu şekildedir.

Bir ağaç sözdizim ayrıştırıcı ile yapılan ifadeleri irdeleyebiliriz. Örnek işlemimizin LISP stili ağaç yapısı şu şekildedir.
(+ (+ 1 (/ 15 5)) (* 1 1))
Böyle bir ağaç yapısını ayrıştırmamız için gelen verileri analiz edecek bir de ağaç çözümleyiciye (tree parser) ihtiyacımız bulunmaktadır. Ağaç çözümleyicinin kuralları şu şekildedir.
tree grammar HesapMakinesiTreeParser;
options {
tokenVocab = HesapMakinesi;
ASTLabelType = Tree;
}
// ### AGAC SOZ DIZIM COZUMLEYICI - AYRISTIRICI (PARSER) KURALLARI
basla
: a = ifade {System.out.println(a);}
;
ifade returns [int ifd]
: ^(ARTI a = ifade b = ifade {ifd = a + b;})
| ^(EKSI a = ifade b = ifade {ifd = a - b;})
| ^(CARPI a = ifade b = ifade {ifd = a * b;})
| ^(BOLU a = ifade b = ifade {ifd = a / b;})
| s = SAYI {ifd = (int) new Integer(s.getText());}
;
Yukarıdaki antlr tarzı BNF imlasında (+ (+ 1 (/ 15 5)) (* 1 1)) ağaç yapısı yukarıdan aşağı doğru işletilir. Kıvrık parantezlerdeki Java kodları bir çok ifade oluşturup nihayet kök kural olan 'basla' biterken konsola bulunan ifade yazdırılır.
Bu kurallar ile antlr kullanılarak 3 adet dosya elde ederiz. HesapMakinesiLexer.java, HesapMakinesiParser.java, HesapMakinesiTreeParser.java bu 3 sınıfı çağıran ana programımız ise şu şekilde olabilir.
import org.antlr.runtime.*;
import org.antlr.runtime.tree.*;
import jline.*;
public class HesapMakinesi {
public static void main(String[] args) throws Exception {
ConsoleReader okuyucu = new ConsoleReader();
String satir;
while ((satir = okuyucu.readLine(">>> ")) != null) {
CharStream karakter_katari = new ANTLRStringStream(satir);
HesapMakinesiLexer sozcuk_cozumleyici = new HesapMakinesiLexer(karakter_katari);
CommonTokenStream sozcukler = new CommonTokenStream(sozcuk_cozumleyici);
HesapMakinesiParser sozdizim_cozumleyici = new HesapMakinesiParser(sozcukler);
HesapMakinesiParser.basla_return agac = sozdizim_cozumleyici.basla();
// AGAC YAPISI LISP STILI GORUNTULEMEK ICIN ASAGIDAKI IFADE KULLANILABILIR
System.out.println("agac yapısı : " + ((Tree)agac.tree).toStringTree());
CommonTreeNodeStream dugumler = new CommonTreeNodeStream((Tree)agac.tree);
HesapMakinesiTreeParser agac_cozumleyici = new HesapMakinesiTreeParser(dugumler);
agac_cozumleyici.basla();
}
}
}
|