Yazılım dilimizi formal tanımlayarak başlayalım:
⟨S⟩ → id = ⟨Expr⟩ ; | print ⟨Expr⟩ ;
⟨Expr⟩ → ⟨Term⟩ + ⟨Expr⟩ | ⟨Term⟩ - ⟨Expr⟩ | ⟨Term⟩
⟨Term⟩ → ⟨Factor⟩ * ⟨Term⟩ | ⟨Factor⟩ / ⟨Term⟩ | ⟨Factor⟩
⟨Factor⟩ → - ⟨Atom⟩ | + ⟨Atom⟩ | ⟨Atom⟩
⟨Atom⟩ → id | num | ( ⟨Expr⟩ )
Tokenizer'ın tanımladığı TK_INT, TK_FLOAT ve TK_IDENT'in yanında bizim kendi tanımladığımız tokenlerimiz olacak:
TK_PRINT,
TK_LPAREN, TK_RPAREN,
TK_PLUS, TK_MINUS,
TK_STAR, TK_SLASH,
TK_EQ,
TK_SEMI,
};
Matematiksel ifade ve değişken içeren, basit bir dil. Bir önceki sayfada, benzer bir gramerin oluşturduğu ağacı göstermiştik, hatırlayın. Şimdi o ağacın tam nasıl oluşturulduğunu inceleyeceğiz.
Top-Down Parsing adı verilen yöntemde, başlangıç sembolünden başlayarak (örneğin ⟨S⟩) yaprakları aşağı doğru uzanan bir ağaç oluşturulur. Nonterminal'ler sırasıyla daha alt kurallara genişletilir ve en uçtaki kurallar da tamamlanana kadar depth-first yol izlenir. x = -5; ifadesini ele alalım:
Bu yöntem için önce recursive fonksiyon kullanan parser yazacağız. Sonrasında top-down parsing'deki sorunlarını ve elle her bir kural için fonksiyon yazmanın zorluğunu çözen bir kütüphane kullanacağız.
Recursive Descent Parser
};
struct map variables
Tanımlı değişkenler.
Parser'ın iç yapısı biraz karışık gelebilir. Parser elemanlarını yeri geldikçe açıklayacağız. Parser'ın esas fonksiyonlarıysa:
void parser_eval(struct parser *parser, const char *text)
Metin girdisini evaluate eder.
void parser_init(struct parser *parser)
[Token ID'leri]
Top-down parser'ımız tokenleri okurken evaluate edecek. Şimdilik parser ve evaluation kısmını birleşik yazacağız, somut bir tree yapısı olmayacak.
Basmakalıp init/destroy fonksiyonlarımızı tanımlayalım:
{
}
{
}
void map_destroy(struct map *map)
map tarafından ayrılmış belleği temizler.
void map_init(struct map *map, size_t value_size)
Yeni bir map oluşturur.
void parser_destroy(struct parser *parser)
[Parser'ın işlevleri]
struct token token
Eşleştirilen token.
struct tokenizer tokenizer
Keyword ve punctuation'ımızı kaydettiğimiz tokenizer.
size_t id
Token tipini ifade eden özgün ID.
void tokenizer_destroy(struct tokenizer *tokenizer)
Tokenizerın tahsis ettiği belleği temizler.
void tokenizer_add_keyword(struct tokenizer *tokenizer, const char *keyword, size_t id)
Tokenizera bir keyword kaydeder.
void tokenizer_add_punctuation(struct tokenizer *tokenizer, const char *punctuation, size_t id)
Tokenizera bir punctuation kaydeder..
static const size_t TK_NOTOKEN
tokenizer_feed() ile verilen lexemenin bittiğini belirtir.
void tokenizer_init(struct tokenizer *tokenizer)
[Token tanımı]
Lexer'da kullandığımız advance/peek tasarımını yine kullanıyoruz:
static size_t current_id(
struct parser *p)
{
}
{
return p->token.seminfo;
}
static void consume(
struct parser *p)
{
return;
} else {
return consume(p);
}
}
}
{
if (current_id(p) ==
tk_id) {
consume(p);
return true;
}
return false;
}
struct lexeme lexer_next(struct lexer *lexer)
Sıradaki lexeme.
enum lexeme::lexeme_kind kind
struct lexer lexer
Metin girdisini saran lexer.
void tokenizer_feed(struct tokenizer *tokenizer, struct lexeme lexeme)
Lexemeyi tokenizere gönderir.
struct token tokenizer_next(struct tokenizer *tokenzier)
Sıradaki tokeni çek.
Artık parser_eval() fonksiyonunu yazmaya başlayabiliriz:
{
consume(p);
do {
if (match(p, TK_PRINT)) {
eval_print(p);
continue;
}
eval_asgn(p);
continue;
}
assert(0 && "Beklenmeyen token! IDENT ya da PRINT gelmeliydi.");
}
void eval_print(
struct parser *p);
void eval_asgn(
struct parser *p);
double eval_expr(
struct parser *p);
double eval_term(
struct parser *p);
double eval_factor(
struct parser *p);
double eval_atom(
struct parser *p);
void lexer_init(struct lexer *lexer, const char *text)
[Lexeme]
static const size_t TK_IDENT
Identifier token ID'si.
Evaluation Fonksiyonları
Dilimizde iki çeşit statement var: print ve asgn. print, bir ifadeyi evaluate edip terminale yazdırırken asgn, onu değişkene kaydediyor.
void eval_print(
struct parser *p)
{
double value = eval_expr(p);
assert(match(p, TK_SEMI));
printf("> %.2lf\n", value);
}
void eval_asgn(
struct parser *p)
{
size_t id = current_seminfo(p).ident_id;
consume(p);
assert(match(p, TK_EQ));
double new_value = eval_expr(p);
assert(match(p, TK_SEMI));
if (value)
*value = new_value;
else
}
void map_insert2(struct map *map, const void *key, size_t keylen, const void *value)
map'e key-value ikilisini ekler.
void * map_get2(struct map *map, const void *key, size_t keylen)
Key ile eşleşen valueyı döner, key bulunamazsa NULL döner.
Benzer mantıkla tanımlanmış expr, term ve factor kurallarını match ediyoruz:
double eval_expr(
struct parser *p)
{
double lhs = eval_term(p);
if (match(p, TK_PLUS)) {
double rhs = eval_expr(p);
lhs += rhs;
} else if (match(p, TK_MINUS)) {
double rhs = eval_expr(p);
lhs -= rhs;
}
return lhs;
}
double eval_term(
struct parser *p)
{
double lhs = eval_factor(p);
if (match(p, TK_STAR)) {
double rhs = eval_term(p);
lhs *= rhs;
} else if (match(p, TK_SLASH)) {
double rhs = eval_term(p);
lhs /= rhs;
}
return lhs;
}
double eval_factor(
struct parser *p)
{
if (match(p, TK_PLUS))
return eval_atom(p);
else if (match(p, TK_MINUS))
return -eval_atom(p);
else
return eval_atom(p);
}
Son olarak da sayısal değer ifade eden en küçük birimleri, atomları evaluate ediyoruz.
double eval_atom(
struct parser *p)
{
union seminfo s = current_seminfo(p);
sizeof(size_t));
return value ? *value : 0;
} else if (match(p, TK_LPAREN)) {
double expr = eval_expr(p);
assert(match(p, TK_RPAREN));
return expr;
}
assert(0 && "Syntax error.");
}
static const size_t TK_FLOAT
Ondalık sayı token ID'si.
static const size_t TK_INT
Tam sayı token ID'si.
size_t ident_id
TK_IDENT tipi için seminfo.
intmax_t num_int
TK_INT tipi için seminfo.
double num_float
TK_FLOAT tipi için seminfo.
İşlem Önceliği...
Bu kadar! Artık değişken ve matematiksel ifade içeren bir yazılım dilimiz var... sanıyorsanız yanılıyorsunuz. Yazdığımız koda şu ifadeyi verirsek büyük bir hüsrana uğrarız: print 10 - 2 - 3;
Bu işlemin matematiksel sonucu 5 olmalıdır. Fakat bizim parser'ımız ekrana 11 yazdırır! Top-down parsing'de parser'la evaluation'ı birlikte yazdığımız zaman işlem önceliğinin ters olmasından kurtulmanın kolay bir yolu yok. Bu sorunu nasıl çözeceğimizi, top-down parser'ların Mental Model'ini inceleyerek göreceğiz.