SEN2001 · 3. hafta · Describing Syntax and Semantics II

Lexical ve syntax analysis: lexer, recursive descent, LL ve LR

Lexer'ın lexeme/token üretmesi, durum diyagramı, recursive-descent parser izleme, left recursion kaldırma, pairwise disjointness testi ve bottom-up (shift-reduce) fikri.

lexical analyzersyntax analyzerlexemetokenstate diagramfinite automatontop-down parserbottom-up parserrecursive descentLL parserLR parserleft recursionpairwise disjointnessFIRST setleft factoringhandleshift-reduce

Bir derleyicinin ilk iki aşaması lexical analysis (sözcük analizi) ve syntax analysis (sözdizimi analizi, parsing). Vizede en çok çıkan soru: verilen recursive-descent fonksiyonlarıyla bir ifadenin ayrıştırmasını adım adım izlemek. Ayrıca left recursion kaldırma ve pairwise disjointness testi kısa, net puanlar.

Altın kurallar:

  1. Lexer token üretir, parser ağaç kurar.
  2. Recursive descent’te her nonterminal bir fonksiyon. { } while döngüsü, [ ] if.
  3. lex() her token için bir kez çağrılır ve bir sonraki token’ı nextToken’a koyar.
  4. Left recursion top-down parser’ı öldürür, kaldırılmalı.
  5. LL = top-down, LR = bottom-up.

Neden iki ayrı aşama?

NedenAçıklama
SimplicityLexer basit, düzenli (regular) örüntüler tanır. Parser’ı bu ayrıntıdan kurtarır.
EfficiencyDerleme süresinin büyük kısmı karakter karakter okumada geçer; lexer ayrıca optimize edilir.
PortabilityDosya okuma, tampon gibi platforma bağlı işler lexer’da toplanır. Parser taşınabilir kalır.

Lexical analysis

Lexer kaynak kodu karakter karakter okur, karakterleri lexeme’lere toplar ve her lexeme’e bir token verir. Boşlukları ve yorumları atlar, kullanıcı tanımlı isimleri symbol table’a ekler, hataları (geçersiz karakter) bildirir.

result = rate * (count + 15);

IDENT(result) ASSIGN_OP(=) IDENT(rate) MULT_OP(*) LEFT_PAREN(()
IDENT(count) ADD_OP(+) INT_LIT(15) RIGHT_PAREN()) SEMICOLON(;)

10 lexeme. Lexer bir sonlu otomat (finite automaton) gibi çalışır; bunu çizmenin yolu durum diyagramı (state diagram):

            letter/digit
             ┌────┐
   letter    ▼    │
Start ─────► id ──┘ ── başka karakter ──► return IDENT (ya da reserved word)
  │
  │ digit    digit
  │         ┌────┐
  └───────► int ◄┘ ── başka karakter ──► return INT_LIT

Her harf ve her rakam için ayrı ok çizmek yerine karakter sınıfları kullanılır: LETTER ve DIGIT. Reserved word’ler (if, while) ayrı durum almaz; identifier olarak tanınır, sonra bir tabloya bakılarak ayrılır.

Parsing: top-down ve bottom-up

Parser iki iş yapar: sözdizimi hatalarını bulmak ve parse tree’yi (ya da onun bilgisini) üretmek.

Top-downBottom-up
AğaçKökten yapraklara, preorderYapraklardan köke
TüretmeLeftmostRightmost, tersten
Ana kararHangi RHS seçilecek?Hangi alt dizi indirgenecek (handle)?
AileLL (recursive descent, LL tablo)LR (shift-reduce)

LL: Left-to-right okuma, Leftmost derivation. LR: Left-to-right okuma, Rightmost derivation (tersten). Her türlü belirsiz olmayan gramer için genel parse algoritmaları O(n3)O(n^3); derleyicilerde kullanılan LL ve LR parser’lar gramerin bir alt sınıfıyla çalışır ve O(n)O(n)‘dir.

Recursive descent

Her nonterminal için bir fonksiyon yazılır. EBNF ile çok doğal eşleşir:

/* <expr> -> <term> {(+ | -) <term>} */
void expr() {
  term();
  while (nextToken == ADD_OP || nextToken == SUB_OP) {
    lex();
    term();
  }
}
/* <term> -> <factor> {(* | /) <factor>} */
void term() {
  factor();
  while (nextToken == MULT_OP || nextToken == DIV_OP) {
    lex();
    factor();
  }
}
/* <factor> -> id | int_constant | ( <expr> ) */
void factor() {
  if (nextToken == IDENT || nextToken == INT_LIT)
    lex();
  else if (nextToken == LEFT_PAREN) {
    lex();
    expr();
    if (nextToken == RIGHT_PAREN) lex();
    else error();
  }
  else error();
}

Kural: bir fonksiyon çağrıldığında nextToken zaten o fonksiyonun ilk token’ını tutuyor. Program lex() ile başlar, sonra expr() çağrılır.

İzleme: (a + b) * c

Enter <expr>
  Enter <term>
    Enter <factor>            next = (  → lex, next = a
      Enter <expr>
        Enter <term>
          Enter <factor>      next = a  → lex, next = +
          Exit <factor>
        Exit <term>           + çarpma değil, term döner
        lex, next = b         expr'nin while'ı + gördü
        Enter <term>
          Enter <factor>      next = b  → lex, next = )
          Exit <factor>
        Exit <term>
      Exit <expr>             ) toplama değil
      lex, next = *           factor ) bekliyordu, buldu
    Exit <factor>
    lex, next = c             term'in while'ı * gördü
    Enter <factor>            next = c  → lex, next = EOF
    Exit <factor>
  Exit <term>
Exit <expr>

Sayım: factor 4 kez, term 3 kez, expr 2 kez girildi, lex 7 kez çağrıldı. Sınavda izleme yaparken her satırda “nextToken şu an ne?” sorusunu sor. Döngü koşulu her zaman o anki nextToken’a bakar.

TUZAK · hangi döngü yakalar? x - y / 2 için - işaretini term değil expr’nin döngüsü yakalar (term sadece * ve / arar). / ise ikinci term() çağrısının içinde işlenir. Böylece ağaç x - (y / 2) olur ve öncelik kendiliğinden doğru çıkar.

<list> -> id { , id } için de aynı mantık: id , id , girdisinde son virgülden sonra id beklenirken EOF gelir, hata.

LL gramerin iki problemi

1. Left recursion

<expr> -> <expr> + <term> | <term>

expr() ilk iş olarak expr()’yi çağırır, hiç token tüketmeden. Sonsuz özyineleme. Doğrudan left recursion kaldırma kalıbı:

A→Aα∣β⟹A→βA′,A′→αA′∣εA \to A\alpha \mid \beta \quad\Longrightarrow\quad A \to \beta A', \quad A' \to \alpha A' \mid \varepsilon

<T>  -> <T> * <F> | <T> / <F> | <F>
⇓
<T>  -> <F> <T'>
<T'> -> * <F> <T'> | / <F> <T'> | ε

Yeni gramer aynı dili üretir (id, id * id, id / id * id, …). Sadece yapısı farklı.

2. Pairwise disjointness

Parser, bir sonraki token’a bakarak hangi RHS’yi seçeceğini bilmeli. Bunun için bir nonterminal’in her iki alternatifinin FIRST kümeleri kesişmemeli. FIRST(α), α’dan türeyen dizilerin ilk terminallerinin kümesi.

<A> -> a <B> | <B> b         FIRST(a<B>) = {a}
<B> -> a <C> | d             FIRST(<B>b) = FIRST(<B>) = {a, d}
                             {a} ∩ {a, d} = {a}  → TEST BAŞARISIZ

Düzeltme left factoring: ortak öneki dışarı al.

<stmt> -> if <c> then <s> | if <c> then <s> else <s>
⇓
<stmt> -> if <c> then <s> <rest>
<rest> -> else <s> | ε

Bottom-up parsing

Bottom-up parser girdiyi soldan okur, token’ları bir yığına koyar (shift) ve yığının üstündeki dizi bir RHS ile eşleştiğinde onu LHS’ye indirger (reduce). İndirgenecek doğru alt diziye handle denir. Sonuç, rightmost derivation’ın tersten yazılmış halidir:

id + id * id
<factor> + id * id         handle: ilk id
<term> + id * id
<expr> + id * id
<expr> + <factor> * id
<expr> + <term> * id       burada <expr> + <term> indirgenmez, * bekliyor
<expr> + <term> * <factor>
<expr> + <term>
<expr>

8 indirgeme. Zor kısım handle’ı doğru seçmek: <expr> + <term> bir RHS ama sonrasında * geldiği için şimdi indirgenirse çarpma yanlış bağlanır. LR parser’lar bu kararı bir tablo (ACTION/GOTO) ile verir; tabloyu kurmak bu dersin vize kapsamı dışında, fikri bilmek yeterli.

Sık yapılan hatalar

YanlışDoğrusu
Boşluğu lexeme saymakLexer boşluk ve yorumları atlar
lex() fonksiyon girişinde çağrılır sanmakSadece bir token tüketilince çağrılır
- işaretini term’in döngüsünde aramak+ - expr’de, * / term’de
Left recursion’ı sağ özyinelemeye çevirip ε’yi unutmakA' -> α A' | ε
FIRST(<B> b) = {b}İlk sembol <B>, onun FIRST’üne bak
LR = Left-to-right, Rightmost (düz)Rightmost derivation tersten
Bottom-up = preorderPreorder top-down’dır

Kendini test et

Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.

Soru 1kolay

How many lexemes are in the statement below? Give the token of 15 and of (.

result = rate * (count + 15);
Cevabı göster
10; INT_LIT and LEFT_PAREN

result, =, rate, *, (, count, +, 15, ), ; → 10 lexeme. Boşluklar lexeme değildir, lexer atlar. Üç farklı isim (result, rate, count) aynı token'ı alır: IDENT.

Soru 2kolay

A lexer recognizes identifiers with the pattern letter (letter | digit)*. Which are identifiers: x1, 1x, abc9, a_b, Z?

Cevabı göster
x1, abc9, Z

İlk karakter harf olmalı: 1x ilk durumda harf beklerken rakam görür, reddedilir. a_b: alt çizgi örüntüde yok, a'dan sonra lexer durur; a_b tek bir identifier olarak tanınmaz. Z tek harf, (letter | digit)* sıfır tekrar olabilir.

Soru 3orta

Using the recursive-descent functions for <expr> -> <term> {(+|-) <term>}, <term> -> <factor> {(*|/) <factor>}, <factor> -> id | int_constant | ( <expr> ), how many times is factor() entered and how many times is lex() called while parsing (a + b) * c? (The first token is already read before expr() is called.)

Cevabı göster
factor: 4 times; lex: 7 times

factor çağrıları: dıştaki ( ... ), içerideki a, içerideki b, sonra c → 4. lex her token için bir kez çağrılır (yeni nextToken'ı getirir): (, a, +, b, ), *, c tüketilir → 7. expr 2 kez (dış ve parantez içi), term 3 kez girilir.

Soru 4orta

Give the first six lines of the Enter/Exit trace when parsing x - y / 2 with the same functions.

Cevabı göster
Enter <expr>, Enter <term>, Enter <factor>, lex (next = -), Exit <factor>, Exit <term>

Enter <expr>
  Enter <term>
    Enter <factor>      next = x (IDENT) → lex, next = -
    Exit <factor>
  Exit <term>           - çarpma/bölme değil, term döner
  expr sees - → lex, next = y
  Enter <term> ... y / 2 burada (factor, lex /, factor)
- term'in değil expr'in döngüsünde yakalanır; / ise ikinci term'in içinde. Toplam 5 lex, 3 factor.

Soru 5kolay

What happens when the recursive-descent parser above is given (a + b (missing right parenthesis)?

Cevabı göster
error() is called in factor: expected ) but found EOF

factor ( görür, lex eder, expr'yi çağırır. expr a + b'yi işler ve döner; nextToken EOF. factor ) bekler, bulamaz → error(). Recursive descent hatayı tam olarak beklentinin bozulduğu fonksiyonda yakalar.

Soru 6orta

Perform the pairwise disjointness test on <A>. Does the grammar pass?

<A> -> a <B> | <B> b
<B> -> a <C> | d
<C> -> c
Cevabı göster
No: FIRST(a<B>) = {a}, FIRST(<B>b) = {a, d}, they share a

<B> b alternatifinin FIRST kümesi <B>'nin ne ile başladığına bağlı: a <C> → a, d → d. İki alternatif de a ile başlayabildiği için parser tek token'a bakarak hangisini seçeceğini bilemez. Test başarısız. <B> kuralı ise ({a} ve {d}) geçer.

Soru 7kolay

Does <S> pass the pairwise disjointness test?

<S> -> x <S> | <T> y | z
<T> -> w <T> | v
Cevabı göster
Yes: {x}, {w, v}, {z} are pairwise disjoint

FIRST(x<S>) = {x}. FIRST(<T>y) = FIRST(<T>) = {w, v}. FIRST(z) = {z}. İkişer ikişer kesişim boş → test geçer, tek token lookahead yeterli.

Soru 8orta

Remove the left recursion from: <T> -> <T> * <F> | <T> / <F> | <F>

Cevabı göster
<T> -> <F> <T'> ; <T'> -> * <F> <T'> | / <F> <T'> | ε

Kalıp: A→Aα1∣Aα2∣βA \to A\alpha_1 \mid A\alpha_2 \mid \beta ⇒ A→βA′A \to \beta A', A′→α1A′∣α2A′∣εA' \to \alpha_1 A' \mid \alpha_2 A' \mid \varepsilon. Burada β = <F>, α1 = * <F>, α2 = / <F>. Yeni gramer aynı dili üretir ama recursive descent'te T() artık kendini token tüketmeden çağırmaz.

Soru 9orta

Left-factor the rule: <stmt> -> if <c> then <s> | if <c> then <s> else <s>

Cevabı göster
<stmt> -> if <c> then <s> <rest> ; <rest> -> else <s> | ε

İki alternatif aynı önekle (if <c> then <s>) başlıyor, FIRST kümeleri çakışıyor. Ortak öneki dışarı al, farklı kalan kısmı yeni bir nonterminal'e koy. EBNF ile: <stmt> -> if <c> then <s> [else <s>], recursive descent'te [ ] bir if olur.

Soru 10zor

In a bottom-up (shift-reduce) parse of id + id * id with the expr/term/factor grammar, what is the first handle reduced, and how many reductions are performed in total?

Cevabı göster
the first id (factor -> id); 8 reductions

Bottom-up parser rightmost türetmeyi tersten kurar. İndirgeme sırası:

id + id * id
<factor> + id * id
<term> + id * id
<expr> + id * id
<expr> + <factor> * id
<expr> + <term> * id
<expr> + <term> * <factor>
<expr> + <term>
<expr>
8 indirgeme = ağaçtaki iç düğüm sayısı. Dikkat: <expr> + <term> * id aşamasında <expr> + <term> indirgenmez; handle id'dir çünkü * daha önce bağlanmalı.

Soru 11kolay

What do the letters in LL and LR stand for, and which one builds the parse tree from the leaves up?

Cevabı göster
LL: Left-to-right scan, Leftmost derivation; LR: Left-to-right scan, Rightmost derivation (reversed); LR is bottom-up

İki türde de girdi soldan sağa okunur. LL top-down'dır, kökten başlar ve ağacı preorder kurar (recursive descent bir LL parser'dır). LR bottom-up'tır, yapraklardan köke doğru handle bulup indirger (shift-reduce).

Soru 12kolay

Give the three reasons for separating lexical analysis from syntax analysis.

Cevabı göster
simplicity, efficiency, portability

Simplicity: lexer basit bir örüntü eşleyici, parser'ı küçültür. Efficiency: derleme süresinin büyük kısmı lexer'da geçer, ayrı optimize edilebilir. Portability: lexer dosya okuma gibi platforma bağlı işleri yapar, parser taşınabilir kalır.

Doğrulama: Lexeme listeleri bir durum diyagramı lexer'ıyla, recursive-descent izlemeleri (Enter/Exit, lex sayıları) aynı EBNF fonksiyonlarını node'da çalıştırarak, FIRST kümeleri ve left recursion kaldırmanın dili değiştirmediği (uzunluk 5'e kadar tüm diziler) node ile kontrol edildi; bottom-up indirgeme sırası rightmost türetmenin tersinden alındı (verify/SEN2001/03b.mjs).

Deftere not kartı

lexer: karakter → lexeme → token, boşluk/yorum atlar, isimleri symbol table'a lexer = finite automaton / regular grammar ; parser = context-free ayırma nedenleri: simplicity, efficiency, portability top-down (LL): kökten, leftmost, preorder. recursive descent = LL bottom-up (LR): yapraklardan, rightmost TERSTEN, shift-reduce, handle RD: her nonterminal = bir fonksiyon, { } = while, [ ] = if, | = if/else lex() = bir sonraki token'ı nextToken'a koy. lex sayısı = token sayısı left recursion A -> A a | b ⇒ A -> b A' ; A' -> a A' | ε left recursion RD'yi sonsuz özyinelemeye sokar pairwise disjointness: alternatiflerin FIRST kümeleri kesişmemeli bozuksa left factoring: S -> a X | a Y ⇒ S -> a S' ; S' -> X | Y genel parse O(n^3), LL/LR O(n)