Derleyici Tasarımı
Yüklüyor...
Arıyor...
Eşleşme Yok
Top-Down Parsing

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:

enum token_id {
TK_PRINT,
TK_LPAREN, TK_RPAREN,
TK_PLUS, TK_MINUS,
TK_STAR, TK_SLASH,
TK_EQ,
TK_SEMI,
};
token_id
[Parser tanımı]

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 parser {
struct map variables;
struct token token;
struct lexer lexer;
};
lexer.
Definition lexer.h:14
map.
Definition map.h:18
Parser.
struct map variables
Tanımlı değişkenler.
Token.
Definition tokenizer.h:61
tokenizer.
Definition tokenizer.h:33

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_init(struct parser *parser);
void parser_eval(struct parser *parser, const char *text);
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 parser_init(struct parser *p)
{
/* Değişkenlerin değerini map'te tutacağız. */
map_init(&p->variables, sizeof(double));
/* Eşleştirilmekte olan token. */
/* Dilimizdeki keyword ve punctuation'ları tokenizer'a bildirelim. */
tokenizer_add_keyword(&p->tokenizer, "print", TK_PRINT);
tokenizer_add_punctuation(&p->tokenizer, "(", TK_LPAREN);
tokenizer_add_punctuation(&p->tokenizer, ")", TK_RPAREN);
tokenizer_add_punctuation(&p->tokenizer, "-", TK_MINUS);
tokenizer_add_punctuation(&p->tokenizer, "/", TK_SLASH);
}
/* Basmakalıp destroyer fonksiyonu. */
void parser_destroy(struct parser *p)
{
}
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.
Definition tokenizer.h:66
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.
Definition tokenizer.h:19
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.id;
}
static union seminfo current_seminfo(struct parser *p)
{
return p->token.seminfo;
}
/* Sıradaki token'a geçer. */
static void consume(struct parser *p)
{
/* Tokenizer, lexeme'yi tüketmişse yeni lexeme çek. */
if (current_id(p) == TK_NOTOKEN) {
struct lexeme next_lexeme = lexer_next(&p->lexer);
/* Lexeme'ler tükenmişse token TK_NOTOKEN olarak işaretle. */
if (next_lexeme.kind == LEXEME_EOF) {
return;
} else {
/* Yeni lexeme gelmişse ona geçmek için bu fonksiyonu
* tekrar çağır. */
tokenizer_feed(&p->tokenizer, next_lexeme);
return consume(p);
}
}
}
/* Beklenen token geldiyse tüketir ve true döner. */
static bool match(struct parser *p, size_t tk_id)
{
if (current_id(p) == tk_id) {
consume(p);
return true;
}
return false;
}
struct lexeme lexer_next(struct lexer *lexer)
Sıradaki lexeme.
tk_id
Token ID.
Ham lexer çıktısı.
Definition lexer.h:21
enum lexeme::lexeme_kind kind
@ LEXEME_EOF
Definition lexer.h:31
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.
[Tokenizer tanımı]
Definition tokenizer.h:51

Artık parser_eval() fonksiyonunu yazmaya başlayabiliriz:

void parser_eval(struct parser *p, const char *text)
{
lexer_init(&p->lexer, text);
/* init fonksiyonundan kalma TK_NOTOKEN'ı consume et. */
consume(p);
/* Tokenler bitmediği sürece devam et. */
do {
if (match(p, TK_PRINT)) {
eval_print(p);
continue;
}
if (current_id(p) == TK_IDENT) {
eval_asgn(p);
continue;
}
assert(0 && "Beklenmeyen token! IDENT ya da PRINT gelmeliydi."); // GCOVR_EXCL_LINE
} while (current_id(p) != TK_NOTOKEN);
}
/* Bütün production rule'ları fonksiyon olarak tanımlayacağız. */
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.
Definition tokenizer.h:28

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)
{
/* print'in hemen sonrasında bir expression olmalı. */
double value = eval_expr(p);
/* expression bitiminde ; olduğundan emin ol. */
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);
/* eval fonksiyonunda id match etmiştik, ondan sonra = olmalı. */
assert(match(p, TK_EQ));
/* ='den sonraki expression'u evaluate et ve değişkene ata. */
double new_value = eval_expr(p);
assert(match(p, TK_SEMI));
double *value = map_get2(&p->variables, &id, sizeof(size_t));
if (value)
*value = new_value;
else
map_insert2(&p->variables, &id, sizeof(size_t), &new_value);
}
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);
/* expression, toplama ya da çıkarma içeriyorsa dallanmaya devam et. */
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);
/* term, expression'a benzer şekilde dallanır. */
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)
{
/* `match` fonksiyonu, bir sonraki tokene geçeceği için seminfo'yu
* kaydet. */
union seminfo s = current_seminfo(p);
if (match(p, TK_INT)) {
return (double) s.num_int;
} else if (match(p, TK_FLOAT)) {
return s.num_float;
} else if (match(p, TK_IDENT)) {
double *value = map_get2(&p->variables,
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."); // GCOVR_EXCL_LINE
}
static const size_t TK_FLOAT
Ondalık sayı token ID'si.
Definition tokenizer.h:25
static const size_t TK_INT
Tam sayı token ID'si.
Definition tokenizer.h:22
size_t ident_id
TK_IDENT tipi için seminfo.
Definition tokenizer.h:57
intmax_t num_int
TK_INT tipi için seminfo.
Definition tokenizer.h:55
double num_float
TK_FLOAT tipi için seminfo.
Definition tokenizer.h:53

İş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.