phpeg – PEG pro PHP
Napsal jsem pacc a doufal jsem, že co se týče parsování něčeho v PHP, mám vystaráno, že mě už nic nezaskočí. Jak jsem se mýlil!
pacc (stejně jako mnoho dalších nástrojů) pracuje s bezkontextovými gramatikami (dále jen CFG) a používá „dvouúrovňové“ zpracování vstupu:
vstupní text → lexer → parser → výstup
Kdy se ale podobné zpracování nehodí? Např. pokud chcete zpracovat tenhle šablonovací jazyk. Lexer by se musel dost zkomplikovat a hodně by kopíroval procesy dějící se v parseru. Tudy cesta nevede.
A je tu phpeg. phpeg namísto CFG pracuje s PEGy. Stejně jako CFG jsou PEGy druhem formální gramatiky. Hlavním rozdílem je to, že operátor alternativy (u CFG |, u PEGů /) je u PEGů asymetrický, takže pokud uspěje první alternativa, druhá už není vyhodnocována. Jinak si syntaxe bere hodně z regulárních výrazů. Přidávají se ještě dva prefixové operátory – & a ! –, které uspějí tehdy, uspěje-li jimi prefixovaný výraz, ovšem tyto operátory nespořádají žádný vstupní text (resp. pokud vše uspěje kurzor v textu se vrátí na místo, kde by před tím, než-li byl výraz testován).
Doporučuji k pročtení Parsing Expression Grammars: A Recognition-Based Syntactic Foundation (PDF) a Experimenting with Programming Languages (PDF 1,34MB).
Jak začít používat phpeg? Nejdříve si budete muset stáhnout a nainstalovat pacc, proto phpeg ho používá bootstrappování (je sranda snažit se popsat gramatiku PEGu pomocí CFG, protože CFG nemá ! operátor; nepřišel jsem na to jak, takže bootstrapová verze PEG gramatiky phpegu musí mít mezi pravidly oddělovače). Až budete mít pacc nainstalovaný v $PATH, přejděte do adresáře se zdrojovými kódy a spusťte:
$ ./scripts/bootstrap.sh
Jestliže všechno proběhlo v pořádku, měl by se v lib/parse/ nacházet soubor php.php. Nyní můžete phpeg zkompilovat do jednoho spustitelného souboru a umístit třeba do /usr/bin/:
# ./scripts/compile.php /usr/bin/phpeg
Stejně jako u pacc, příklad kalkulačky:
exp = e:exp "+" f:frac -> $e + $f
/ e:exp "-" f:frac -> $e - $f
/ f:frac -> $f
frac = f:frac "*" n:num -> $f * $n
/ f:frac "/" n:num -> $f / $n
/ s "(" e:exp ")" s -> $e
/ n:num -> $n
num = s d:[0-9]+ s -> intval($d)
s = [ \t\r\n]*
PEG je sada pravidel, jak číst vstupní řetězec. Pravidla phpegu začínají nějakým identifikátorem, poté následuje rovnítko a pak samotné pravidlo. K jednolivým částem pravidla se nepřistupuje jako u pacc pomocí pořadového číslo (což by u těch složitějších šlo těžko), nýbrž se musí každá část, se kterou chce člověk pracovat, pojmenovat. To se dělá tak, že se dá identifikátor a dvojtečka před výraz, jehož hodnotu potřebujeme. Za šipkou následuje sémantická hodnota výrazu. Buď se může jednat o jednoduchý výraz, a ten se bere od začátku šipky do konce řádku, nebo o blok (ten se musí obklopit složenými závorkami). Při použití bloku je toto ekvivalentní zápis pravidla num výše:
num = s d:[0-9]+ s -> { return intval($d); }
Příklad se nachází ve zdrojové distribuci v adresáři examples/. Zkompilovat a vyzkoušet ho můžete následovně; přejděte do adresáře se zdrovými kódy a spusťte:
$ ./bin/phpeg -i ./examples/calculator.php.peg -fo ./calculator.php
$ cat >> ./calculator.php
list($ok, $output) = calculator(file_get_contents('php://stdin'));
echo "$output\n";
$ echo "22 * 2 - 2" | php -f ./calculator.php
42
Ve stylu convention over configuration, phpeg vyzkouší, jestli název vstupního, popř. výstupního souboru odpovídá jistému vzoru. Tím vzorem je název parsovací funkce následovaný tečkou, názvem parseru/generátoru, tečkou a příponou peg. Název parsovací funkce můžete též určit pomocí přepínače -p a typ parseru/generátoru zase přepínačem -t (více v nápovědě, -h). Např. pro soubor calculator.php.peg bude název parsovací funkce calculator a bude použit php parser/generátor (v současné době je dostupný pouze php parser/generátor; ale architektura phpeg je postavena tak, že se další dají snadno přidat).
Jestliže nechcete umisťovat funkci do globálního prostoru jmen, použijte a přepínač -n, a phpeg umístí na začátek souboru deklaraci namespace.
phpeg vygeneruje PHP skript s jedinou funkcí s dvěma parametry – řetězcem k parsování a polem možností. Tato funkce inicializuje instanci parsovací třídy řetězcem k parsování a jednotlivé klíče z pole možností namapuje na vlastnosti instance třídy (takže v sémantických akcích můžete používat $this->moje_predana_moznost apod.). Funkce vrací trojici – booleovskou hodnotu značící, jestli vše proběhlo v pořádku; sémantickou hodnotu startovacího pravidla; a pozici, kam se až parser dostal (v případě chyby pozici, kde parsování skončilo chybou).
Nutno přiznat phpeg je pomalejší než pacc. Při bootstrapu se nejdříve vygeneruje ze souboru lib/parse/bootstrap.y pomocí pacc LR parser a uloží se do souboru lib/parse/bootstrap.php. Ten potom přechroustá lib/parse/parse_php.bootstrap.peg a vyplivne soubor lib/parse/php.php. Do třetice všeho dobrého phpeg přegeneruje lib/parse/php.php ze souboru lib/parse/parse_php.php.peg pomocí php parseru/generátoru vygenerovaného v minulé fázi. bootstrap parser/generátor vygenerovaný paccem je asi 5krát rychlejší než php parser/generátor vygenerovaný v poslední fázi. Je tím vykoupeno to, že phpeg, jelikož pracuje s proudem znaků, nikoli tokenů, je v jistých situacích mnohem mocnější než pacc.
Na Wikipedii se můžete dočíst, že PEG je ve své podstatě jazyk pro popis recursive descent parseru. Recursive descent parser ale nezvládá levou rekurzi pravidel. phpeg používá packrat parser s podporou pro přímou levou rekurzi. (V případě, že se v gramatice nachází nepřímá levá rekurze, phpeg zahlásí chybu. Nepřímou levou rekurzi jsem ještě v žádné gramatice nepotřeboval. Až ji budu potřebovat, nejspíše se brzy dostane i do phpegu.) Packrat má ještě tu výhodu, že operuje v lineárním čase i přes neomezený backtracking – všechny mezivýsledky si ukládá (a tak je jeho minusem, že spotřebuje hodně paměti).
vydáno 7. 2. 2010, 23:33:11
