3) DLR - Parser
Parser je velice důležitá část kompilátoru. Jeho úkolem je sestavit abstract syntax tree (AST) z tokenů vygenerovaných scannerem. Jak na to si ukážeme v tomto díle.

Co důležitého na AST?
Scanner pouze vygeneruje list tokenů, které reprezentují kód tak jak byl napsán zleva doprava:
Oproti poli tokenů AST obsahuje jednu důležitou informaci navíc a to je jak má být daný kód vyhodnocen. Například kód “x = 3 * 2 - 4 / 2″ necheme vyhodnotit zleva doprava, ale nejdříve vypočítat “3 * 2″ a “4 / 2″, poté odečíst výsledky “6 - 2″ a následně přiřadit hodnotu do proměnné x. Parser tedy převede kód (tokeny) do stromové struktury podle toho, jak mají být jednotlivé operace provedeny:
=>

Ve srovnání se scannerem je implementace parseru obtížnější. I tak jednoduchý jazyk jakým je IPL1 má celkem mnoho parsovacích pravidel a není úplně jednoduché se na něm učit “základy parsování”. Proto pokud nemáte zkušenosti s parser generátory (jako například Yacc a jemu podobné) tak se můžete podívat na implementaci jednoduššího parseru aritmetických výrazů, který je ke stažení zde. Nicméně pojdmě se podívat na implementaci parseru jazyka IPL1.
Parser
Nejdříve musíme vytvořit datový typ pro generovaný strom. Pro tento účel můžeme použít tzv. discriminated unions v F#. Nepočítám, že každý je s tímto konceptem obeznámen, proto se pokusím discriminated unions vysvětlit.
Discriminated unions
Discriminated unions najdete obvykle ve funkcionálních jazycích jako například F# či Haskell. Jedná se o datový typ, který spojuje různé datové struktury.
Vezměme si například strukturu Person, která má uchovávat všechny důležíté informace o osobě, nicméně struktura uložených dat se liší pokud se jedná o dospělou osobu a pokud se jedná o dítě. V případě dítěte chceme uložit jeho jméno (string), věk (int), otec (Person), matka (Person), zatímco pokud se jedná o dospělou osobu pak uložené informace jsou jméno (string) a název firmy (string), ve které pracuje.
Zde je implementace typu Person za použití discriminated unions:
type Person =
| Adult of string * string
| Child of string * int * Person * Person
Pro každou strukturu je definován tzv. konstruktor - Adult a Child. Konstruktor se nazývá proto, že přes tento název se typ inicializuje:
let p1 = Adult("Andy", "Contoso")
let p2 = Adult("Maria", "Fabrikam")
let p3 = Child("John", 12, p1, p2)
Všechny tři proměnné jsou typu Person.
Použití discriminated union
Používat discriminated unions v F# nebudeme, protože s vygenerovaným AST budeme pracovat až v C#, nicméně pro zajímavost zde ukážu, jak se proměnná takovéhoto typu používá na funkci printPerson, která vytiskne jednotlivé hodnoty struktury do řetězce:
let printPerson = function
| Adult(name, company) -> "Adult: " + name + ", " + company
| Child(name, age, father, mother) -> "Child: " + name + " (" + age
+ "), father - (" + printPerson father
+ "), mother - (" + printPerson mother + ")"
Výsledkem volání printPerson p3 je řetězec:
“Child: John (12), father - (Adult: Andy, Contoso), mother - (Adult: Maria, Fabrikam)”
Datový typ pro AST
Zde je datový typ Tm pro parserem vygenerované AST:
// ast.fs
// Arithmetic operations
type ArithOp =
| Add // +
| Sub // -
| Mul // *
| Div // /
// Relation operations
type RelOp =
| Lt // <
| Leq // =<
| Eq // ==
| Neq // !=
| Geq // >=
| Gt // >
// AST
type Tm =
| Num of int
| String of string
| Ident of string
| True
| False
| AOp of ArithOp * Tm * Tm // operation * left * right
| ROp of RelOp * Tm * Tm // operation * left * right
| Lambda of List
Každý konstruktor representuje uzel, případně list stromu.
Zde je příklad jednoduchého kódu:
if true then 5 else 7
a dopovídajícího AST:
IfElse( True, Num(5), Num(7) )
Parsovací pravidla
Zde přichází to nejdůležitější čímž jsou parsovací pravidla, která sestaví požadovaný AST (šedý kód byl již vysvětlen v předchozím článku):
// parser.fsy
%{
open Ast
%}
%token
- Kód mezi %{ a }% bude vložen přímo do vygenerovaného F# kódu
- %start definuje počáteční pravidlo, kterým parser začíná. Zároveň je podle názvu tohoto pravidla pojmenovaná funkce vygenerovaného parseru, tzn. že v tomto případě parser budeme volat Parser.start(…), protože “start“ se jmenuje počáteční pravidlo
- %type je datový typ vrácené hodnoty parserem. V našem případě je to je Tm reprezentující AST
- %% po těchto znacích jsou očekávána parsovací pravidla
- Konečné pravidlo je tmAtom, které (až na poslední konstruktor pro závorky) nelze dále redukovat
Stavění pravidel
Při sestavování pravidel parseru je vždy nejdůležitější, aby parser zachytil co nejvíce nepovolených konstrukcí, ale zároveň aby nebyl příliš restriktivní, tzn. aby byl schopen zpracovat jakýkoliv validní kód.
Například příliš restriktivním pravidlem pro if-then-else by byl:
| KeyIF tmAtom KeyTHEN tm KeyELSE tm { IfElse($2, $4, $6) }
Toto pravidlo očekává v podmínce tmAtom, takže například kód…
if true then 5 else 7
…by parser byl schopen zpracovat, protože parsování true je přímo v tmAtom, nicméně u kódu
if 2 < 3 then 5 else 7
..by již parser vrátil chybu, protože takovou konstrukci již tmAtom nepovoluje.
Naopak příkladem málo restriktivního pravidla je:
| KeyIF tm KeyTHEN tm KeyELSE tm { IfElse($2, $4, $6) }
Pravidlo tm v podmínce totiž povoluje víceméně jakoukoliv konstrukci, takže pro parser by byl validní kód i například:
if fun x,y => x + y then 5 else 7
Samozřejmě podmínka očekává boolean hodnotu, takže tento kód není validní a chyba by se projevila až za běhu aplikace. Nicměně měli bychom se snažit aby již parser objevil co nejvíce nepovolených konstrukcí, protože pokud chybu objeví parser, tak programátor bude varován už při kompilaci kódu zatímco v tomto případě by programátor na chybu přišel až za běhu aplikace a to pouze v případě, že by byla tato část kódu vykonána.
„Správným“ řešením je:
| KeyIF tmEqPrio KeyTHEN tm KeyELSE tm { IfElse($2, $4, $6) }
což je použito i v našem parseru jazyka IPL1. Označení „spravným“ v uvozovkách jsem použil záměrně, protože ani toto pravidlo není stoprocentní a to proto, že nyní sice definici funkce v podmínce již parser jako chybu nahlásí, nicméně například povolí kód
if 5 then 7 else 3
…což taktéž není správně. Pokud bychom se ale snažili parser zdokonalovat tak, aby objevil co nejvíce nepovolených konstrukcí tak budeme potřebovat více pravidel a implementace parseru se stane o mnoho komplikovanější, proto je lepší hledat určitou vyváženost mezi kvalitou parseru a jeho komplikovaností.
Generování
Jakmile je soubor s parsovacímy pravidly hotov, můžeme z něj vygenerovat F# kód pomocí nástroje fsyacc, který jsme již používali minulou kapitolu a který se nachází ve složce kde je nainstalován F#.
Použití je jednoduché:
fsyacc.exe parser.fsy
Volání parseru
Implementace funkce runParser volající parser a vracející AST:
// return AST
let runParser code =
let lexbuf = Lexing.from_string code // initialize
try
Parser.start Scanner.scan lexbuf
with e ->
let ex = Printf.sprintf "Parser excetion: %s \ncolumn %d" e.Message lexbuf.EndPos.Column
failwith ex
Závěr
Zdrojové kódy můžete stáhnout na konci tohoto tutoriálu. Součástí projektu jsou i unit testy, kde můžete vidět jaké AST jsou pro různé kódy vygenerované.
V další kapitole se již dostaneme k DLR a náš kompilátor jazyka IPL1 dokončíme.

IPL1_02parser.zip


February 28th, 2009 at 4:10 am
if you don’t have time to translate, use translate from google it do it a decent work for me. :) translating this document to english, and please publish the last part of your work because it’s very interesting and without that part, all your work and effort will become nothing..
March 29th, 2009 at 5:18 pm
Anonymous > I wanted to send you the source codes, unfortunatelly you didn’t leave your mail address here. If you’re still interested send me an email to: sturala[@]gmail[.]com