Weblog @ rebex.cz

Weblogy na webu Rebexu
Welcome to Weblog @ rebex.cz Sign in | Help
in Search

Problems presenting features of interest

ANTLR aneb jak nebýt jelen

Dělal jsem teď v C# jakési úpravy SQL vyžadující jeho parsování, a zadavatel navrhl použít ANTLR. Osobně mám po několika pokusech s Bisonem ke generátorům kódu vztah stále neujasněný. Programovat na vyšší úrovni je jistě fajn, ale když má pak člověk v gramatice chybu - což se mi stává s nepříjemnou pravidelností - komplikují extra úrovně abstrakce debugování a generátor, netriviální program třetí strany, je spolehlivým zdrojem opravdu netriviálních problémů. Musím nicméně uznat, že od dřevních dob jaků a bizonů udělala parsovací technologie podstatný pokrok a ANTLR je nástroj rozhodně mocný.

Pokrok má samozřejmě i své stinné stránky. Jak se ANTLR bouřlivě vyvíjí - nová velká verze 3.0 byla uvolněna letos v květnu - dokumentace se poněkud opozdila (s důležitou vyjímkou tištěné Definitivní Reference, jejíž užitečnost, nekoupiv, nemohu posoudit), a i bez dokumentace je vidět, že ANTLR není bez kazu... Jiné mé stížnosti do mailing listu se setkaly z hrobovým tichem, takže nelze vyloučit, že problém byl mezi mou klávesnicí a židlí, ale obecně i autor soudí, že netrpělivě očekávaná verze 3.1, napsaná v ANTRL 3.x - verze 3.0 je ještě pořád v ANTLR 2 - bude pro některé gramatiky generovat maličko jiné jazyky než 3.0.

Co se těch gramatik týče, ANTLR na rozdíl od YACCu derivuje pravidla zleva, jako typický ručně psaný rekurzivní parser, takže pravidla pro ANTLR nesmějí být zleva rekurzivní. Pro YACC běžné konstrukce jako

item_list
: item
| item_list ',' item
;
expr
: expr '+' subexpr
| subexpr
;


subexpr
: subexpr '*' term
| term
;

term
: '(' expr ')'
| id
;
jsou v ANTLRu ilegální a je nutné je přepsat na
item_list
: item ( ',' item )*
;
expr
: subexpr ( '+' subexpr )*
;


subexpr
: term ( '*' term )*
;

term
: '(' expr ')'
| id
;

Syntax opakování - ANTLR má ()*, ()+ i ()?, přičemž znak za závorkou má stejný význam jako kvantifikátor regulárního výrazu - je ještě celkem mechanická změna. Hlubší problém představují gramatiky parsovatelné YACCem (LALR, pro akademicky orientované čtenáře), které nelze parsovat shora dolů s žádným konečným lookaheadem (jsou mimo LL(k)). Takové gramatiky nejsou nijak vyjímečné - LR parsery, redukující vstup zdola nahoru, jsou silnější než LL v tom smyslu, že mohou udržovat stav zahrnující více pravidel pro dlouhý úsek vstupu, dokud ho jednoznačný token nerozsekne.

ANTLR toto omezení řeší tzv. syntaktickými predikáty, což je z mého primitivního pohledu prostě syntax pro parsování metodou pokusu a omylu. Např. jedno z pravidel (gramatiky původně pro ANTLR 2.7.2) parsování podmínek v SQL je

subSearchCondition
:
( NOT )? (
(bracketedSearchCondition) =>
bracketedSearchCondition
| predicate
;
v próze "když se vstup dá parsovat jako ozávorkovaná podmínka, je to ozávorkovaná podmínka, jinak to musí být predikát". Mimochodem token NOT je token, protože začíná velkým písmenem. ANTLR generuje separátní lexer a parser, ale oba ze stejné gramatiky - pravidla pro lexer a pro parser jsou syntakticky rozlišena velikostí jejich prvního písmena.

V jistém smyslu jednodušším příkladem syntaktického predikátu je parsování tradiční konstrukce if then else, známé svou nejednoznačností: else následující po několika vnořených if může technicky patřit ke kterémukoli z nich, a jazyky s if musí pro rozhodnutí tohoto případu specifikovat nějaké extra pravidlo, typicky že každé else patří k nejbližšímu kandidátovi. V syntaxi ANTLR (z tutorialu původně pro ANTLR 2.7.2):

ifStatement
: 'if' '(' expression
')' statement
( ( 'else' ) => 'else' statement )?
;
Někteří lidé prý vidí, že tato konstrukce má správnou semantiku - a my ostatní si to můžeme otestovat...

Pro ladění gramatik existuje sofistikované GUI, ANTLRWorks, které má ovšem z mého hlediska zásadní nevýhodu: podporuje pouze Javu, rodnou platformu ANTLR. Pro celkový projekt to není až takový problém - javovská třída generátoru se dá spustit z příkazové řádky, začlenění generovaného kódu do spustitelného programu je popsáno v online dokumentaci a build se dá automatizovat pro NAnt, ale interaktivní debugger se do takového postroje cpe špatně, takže většinu gramatik jsem testoval přidáním akcí a spuštěním testovacího programu.

ifStatement
@init { string curExpr = null; }
: 'if' '(' e = expression {
curExpr = $e.returnValue;
} ')' statement {
Console.WriteLine("then of {0}", curExpr);
} ( ( 'else' ) => 'else' statement {
Console.WriteLine("else of {0}", curExpr);
} )?
;

Akce jsou psány zhruba v tom programovacím jazyce, který ANTLR generuje - kromě Javy je podporováno i C#, C a další. Kód akce není zkopírován do výstupu generátoru zcela beze změn, nýbrž je interpretován jako šablona pro knihovnu StringTemplate, která ho integruje s generovaným parserem, tj. hlavně nahrazuje jména začínající '$' skutečnými vnitřními proměnnými parseru. Blok @init je užitečný pro lokální deklarace: ANTLR ho umisťuje na začátek funkce implementující specifikované pravidlo a garantuje, že se v průběhu parsování odpovídající části vstupu vykoná právě jednou - což pro kód akcí přirozeně neplatí.

V příkladu nahoře je proměnná e automaticky deklarována jako lokální proměnná metody ifStatement, s typem vraceným metodou expression. Generované metody defaultně nevracejí nic co bych chtěl zpracovávat (buď void, nebo interní typy, podle stylu gramatiky), ale je možné přidat specifické hodnoty - např. expression použitá výše má deklarovaný atribut returnValue, který se nastavuje v jejích akcích:

expression returns [ string returnValue ]
: Number { $returnValue = $Number.text; }
| Identifier { $returnValue = $Identifier.text; }
;

Pravidla lexeru takto dekorovat nelze - vracejí vždy instanci třídy Token, mezi jejíž standardní atributy patří zejména text (v C#; Java má metody getText() a setText()).

Semantické rozdíly lexeru a parseru jsou vůbec mnohem podstatnější, než by naivní uživatel mohl soudit z jejich analogické syntaxe. Patrně nejzásadnějším je, že pravidla lexeru nemají hierarchii - kdykoli lexer rozezná na vstupu token, zahrne do něj co nejvíc znaků a je mu jedno, je-li výsledek z hlediska parseru platný nebo ne. V jednoduchých případech - např. pro tokeny '<', '<=' a '=' - je nejdelší možný token správně, ale často je nutné nastavit ho ručně v akci, např.

fragment
Digit : '0'..'9' ;

fragment
Integer :;

fragment
Real :;

Number :
'.' { $type = '.'; } ( (Digit)+ { $type = Real; } )?
| (Digit)+ { $type = Integer; } ( '.' (Digit)* { $type = Real; } )?
;
Fragmenty jsou tokeny používané pouze v definici jiných tokenů - jinými slovy lexer je nikdy nerozeznává.

A to je snad všechno, co člověk potřebuje, aby mohl ze vstupního kódu udělat tradiční AST - byť samozřejmě zdaleka ne všechno co ANTLR umí. Jen namátkou, má semantické predikáty, stav sdílený mezi pravidly, dvoudimenzionální parsování stromovými gramatikami a spoustu dalších vymožeností, jejichž systematický popis zabral knihu - kdybych s ním měl začít znova, nehledal bych dokumentaci po blozích, ale šel bych ke kováři a investoval padesát dolarů do primárního zdroje...

Published 1. září 2007 9:22 by vbarta

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

 

Problems presenting features of interest said:

Před pár měsíci jsem se tu šířil o generátoru parserů ANTLR a jeho použití při tvorbě front-endu transformujícího

ledna 18, 2008 13:19

Leave a Comment

(required) 
(optional)
(required) 
Submit
Powered by Community Server (Personal Edition), by Telligent Systems