[X] Ein Überblick über Spirit2



  • War ja keiner im IRC :p

    Ausserdem hab ich das ja auch in meinem post #2 erwähnt.
    Wenn dann wenig später ein hkaiser postet ist das wohl eindeutig 😉



  • phlox81 schrieb:

    War ja keiner im IRC :p

    jaaaaaa, ich bin halt auch nicht täglich im IRC. Sollte Xin da sein, haut den an, der hat mich im ICQ.



  • phlox81 schrieb:

    War ja keiner im IRC :p

    Ausserdem hab ich das ja auch in meinem post #2 erwähnt.
    Wenn dann wenig später ein hkaiser postet ist das wohl eindeutig 😉

    Stand das vorhin schon da? 😮
    Verd.... ich sollte wieder anfangen, Kaffee zu trinken, der Entzug bekommt mir nicht. 🤡



  • So, der erste Entwurf ist fertig. Der Rest ist Feintuning, also bitte schon mal Feedback, besonders was Fachliches angeht.
    Daher setze ich den Artikel jetzt auch auf T. 🙂



  • Als Vorbemerkung: ich finde es cool, daß bereits jetzt, noch bevor Spirit2 released ist und auch noch kaum Dokumentation vorhanden ist, Artikel darüber geschrieben werden. Gleichzeitig hat das natürlich zur Folge, daß immer noch Änderungen an Syntax und Semantik erfolgen und daher diese Artikel u.U. nicht immer genau beschreiben, wie Spirit2 wirklich aussehen wird. In diesem Sinne seien mir einige Bemerkungen erlaubt... Der Einfachheit halber habe ich einfach den Artikel vollständig zitiert und meine Kommentare dazwischengemogelt. Ich bin mir nicht sicher wie es bei Euch üblich ist, Artikelentwürfe zu kommentieren, seht es mir bitte nach, wenn ich gegen die Konventionen vertoße.

    phlox81 schrieb:

    1 Vorbemerkungen

    Dieser Artikel beschäftigt sich mit boost::spirit 2.0, welches der Nachfolger von boost::spirit1.x ist.
    Für Einsteiger empfiehlt sich deshalb auch die Lektüre von Boost::Spirit - Eine Einführung,
    da ich nicht auf alle Details der Syntax eingehen werde.

    Auch gilt für die meisten Codebeispiele, das die Namespaces eingebunden werden:

    using namespace boost::spirit;
    using namespace boost::spirit::qi;
    using namespace boost::spirit::ascii;
    using namespace boost::spirit::arg_names;
    using boost::phoenix::ref;
    

    1.1 Was ist Neu? Was ist anders?

    Als allererstes sollte man vielleicht sagen, das Spirit2 nicht unbedingt kompatibel zu seinem Vorgänger ist.
    Zwar veränderte sich zum Beispiel die grundlegende Syntax der Regeln nur wenig bis garnicht, aber Spirit2 wurde komplett neu entwickelt.
    Welches auch zur Folge hat, das es nicht kompatibel zu spirit1.x Code ist.

    Die fehlende Kompatibilität hat wenig mit dem Fakt an sich zu tun, daß es neu entwickelt wurde. Wir haben einfach versucht, die Erfahrungen und Probleme der Nutzer einfließen zu lassen. OTOH glaube ich, daß jeder, der Spirit1 kennt, kaum Probleme mit der neuen Version haben wird. Die Dokumentation von Spirit2 wird daher auch einen 'Migration Guide' enthalten, der die Unterschiede beschreibt.

    phlox81 schrieb:

    Die wohl größte Neuerung ist, das Spirit2 nun über drei neue Toplevel Namespaces verfügt:

    • spirit::lex - ein Modul für die Definition von Lexern
    • spirit::qi - Das Modul für die Definition von Regeln zum parsen von Input
    • spirit::karma - Ein Modul zum Formatieren der Ausgabe

    Auf die Details zu diesen Modulen gehe ich später noch genauer ein.
    Neben diesen neuen Modulen, hat sich auch einiges an der Syntax von Grammatiken und anderen spirit Strukturen getan.
    So ist das gekapselte definition Struct von spirit1.x jetzt nicht mehr in der Grammatikklasse nötig:

    spirit1.x Code:

    struct testgrammar : public boost::spirit::grammar<testgrammar>
    	{
    		template<class ScannerT>
    		struct definition
    		{
    			rule<ScannerT> rrule;
    			
    			definition(testgrammar const& self)
    			{
    				rrule =  /* definition */
    			}
    			rule<ScannerT> start() const
    			{ return rrule; }
    
    		};
    	};
    

    spirit2.0 Code:

    template <typename Iterator>
    struct testgrammar : grammar_def<Iterator, unsigned()>
    // der 2. Templateparameter ist der "Rückgabewert" der Grammatik
    {
        testgrammar()
        {
            start = /* definition */
        }
        // der 2. Templateparameter ist der "Rückgabewert" der Grammatik
        rule<Iterator, unsigned()> start;
    };
    
    • Aus meiner Sicht wäre es besser, die alte Syntax an dieser Stelle nicht zu zitieren, da dies verwirrend sein kann.
    • Der 2. template Parameter ist nicht nur der Rückgabewert, sondern kodiert (ähnlich wie bei den rule's) sowohl die 'Parameter' (inherited attributes) als auch den 'Rückgabewert' (synthesized attribute) der Grammatik. Um die Notation soweit wie möglich an die logische Bedeutung eines non-terminals anzupassen, haben wir die Funktions-Notation gewählt, hier unsigned(), also ein non-terminal, welche keine Parameter erwarted, aber einen unsigned Wert generiert

    phlox81 schrieb:

    2 Drei Beispiele für lex, qi & karma

    Um einen Überblick über die neuen Bereiche in Spirit zu bekommen, ist es am besten sich einige Beispiele selber anzuschauen.
    Leider gibt es noch keine Onlinedokumentation oder ähnliches, aus dem man mehr Informationen ziehen kann.
    Aber es gibt die Möglichkeit im Example Verzeichnis zu lesen.

    2.1 spirit::lex

    Lex ist der neue Bereich für Lexer in Spirit. Es gibt die Möglichkeit auch andere Lexer in Spirit zu verwenden, als den Standardlexer von Spirit. Lex dient dazu, den Zeichenstrom in Tokens auf zu teilen, so das die eigentlichen Regeln des Parsers auf den Token des Lexers und nicht mit dem Eingabestrom arbeitet.
    Dies ist allerdings optional, es besteht in Spirit2.0 kein Zwang einen Lexer zu benutzen.

    Ein kleines Beispiel für die Definition eines Lexers in Spirit2.0:

    template <typename Lexer>
    struct example1_tokens : lexer_def<Lexer> // lexer_def<Lexer> ist die Basisklasse für Lexer in Spirit2.0
    {
        template <typename Self>
        void def (Self& self)
        {
            // define tokens and associate them with the lexer
            identifier = "[a-zA-Z_][a-zA-Z0-9_]*";
            self = token_def<>(',') | '{' | '}' | identifier;
            
            // any token definition to be used as the skip parser during parsing 
            // has to be associated with a separate lexer state (here 'WS') 
            white_space = "[ \\t\\n]+";
            self("WS") = white_space;
        }
        token_def<> identifier, white_space;
    };
    

    Von oben nach unten:
    Als erstes wird von lexer_def<Lexer> eine Lexerklasse abgeleitet. "lexer_def<Lexer>" stellt die Basisklasse für Lexer in Spirit2.0 dar. Danach wird eine Templatemethode für die Definition der Lexerregeln erstellt. Self ist hierbei die Startregel des Lexers. Vorher wird die Tokenregel für identifier definiert, welches wie man hier sieht auch als Regular Expression geht. Die Definition von Self ist dann wieder (fast) Spirittypisch: eine Verkettung von verschiedenen Regeln.
    Die Definition des Skipparsers erfolgt darunter, wieder mit einer RegEx. Damit diese Regel als Skipparser aktiv wird, muss sie mit dem Lexerstatus "WS" verbunden werden. Skipparser bedeuted, das diese Zeichen im Zeichenstrom bei der Tokengenerierung ignoriert/übersprungen werden.

    In der Grammatik kann man dann den verwendeten Lexer angeben, und sogar die Lexerregeln als Grammtikregeln benutzen:

    template <typename Iterator>
    struct example1_grammar : grammar_def<Iterator, token_def<> >
    {
        template <typename TokenDef>
        example1_grammar(TokenDef const& tok)
        {
            start = '{' >> *(tok.identifier >> -char_(',')) >> '}';
        }
    
        rule<Iterator, token_def<> > start;
    };
    

    Die konkrete Anwendung des Lexers sieht dann so aus:

    // Irgendwo in einer Funktion
    // iterator typ für den eigentlichen Inputstrom.
    // iterator type used to expose the underlying input stream
        typedef std::string::const_iterator base_iterator_type;
        
    // Dies ist der Lexertyp um den Input in Tokens zu verwandeln.
    // Wir benutzen eine lexertl basierende Lexerengine
        // This is the lexer type to use to tokenize the input.
        // We use the lexertl based lexer engine.
        typedef lexertl_lexer<base_iterator_type> lexer_type;
        
    // Dies ist der Tokendefinitiontyp (Abgeleitet vom gegebenen Lexertyp)
        // This is the token definition type (derived from the given lexer type).
        typedef example1_tokens<lexer_type> example1_tokens;
        
    // Dies ist der Iteratortyp welcher vom Lexer zurückgegeben wird.
        // This is the iterator type exposed by the lexer 
        typedef lexer<example1_tokens>::iterator_type iterator_type;
    
    // Dies ist die Definition des Grammatiktypens, mit dem Geparsed werden soll.
        // This is the type of the grammar to parse
        typedef example1_grammar<iterator_type> example1_grammar;
    
    // Nun werden die Typdefinitionen genutzt, um den eigentlichen Lexer und die Grammatik zu erstellen.
    // Die Instanzen werden während dem Parsingprozess gebraucht.
        // now we use the types defined above to create the lexer and grammar
        // object instances needed to invoke the parsing process
        example1_tokens tokens;                         // Our token definition
        example1_grammar def (tokens);                  // Our grammar definition
    
        lexer<example1_tokens> lex(tokens);             // Our lexer
        grammar<example1_grammar> calc(def);            // Our parser
    
        std::string str (read_from_file("example1.input"));
    
        // At this point we generate the iterator pair used to expose the
        // tokenized input stream.
        iterator_type iter = lex.begin(str.begin(), str.end());
        iterator_type end = lex.end();
            
        // Parsing is done based on the the token stream, not the character 
        // stream read from the input.
        // Note, how we use the token_def defined above as the skip parser.
        bool r = phrase_parse(iter, end, calc, tokens.white_space);
    

    Die Lexerkomponente von Spirit2 ist eine der neuen Bestandteile, welche sich auf jeden Fall noch verändern werden. So wird es möglich sein, explizite lexer states direkt aus dem Parsevorgang heraus zu steuern. Das wird zur Folge haben, daß die Definition und Nutzung dieser lexer states anders sein wird als oben beschrieben, was widerum Auswirkungen auf die Nutzung von Tokens als Skipparser haben dürfte.
    Die entgültige Syntax für die Lexerkomponente steht noch nicht fest, ich bin mir daher nicht sicher ob es gut ist, die Lexer Komponente überhaupt zu beschreiben.

    phlox81 schrieb:

    2.2 spirit::qi

    Mit spirit::qi gibt es nun auch einen eigenständigen Bereich für die Textverarbeitung (parsen). Es gibt einige Veränderungen in diesem Bereich, der quasi auf die Erfahrungen von Spirit1.x aufsetzt. So haben sich die Grammatiken verändert und einige Operatoren sind neu, Sowie eine neue Möglichkeit für die Fehlerbehandlung.

    Spirit.Karma ist auch Textverarbeitung...
    Richtig ist aber, daß faktisch die gesamte Funktionalität, die Spirit1 bisher ausgemacht hat, nun im namespace boost::spirit::qi implementiert wurde.

    phlox81 schrieb:

    2.2.1 Regeldefinitionen

    So gibt es nun mehrere Möglichkeiten, die Regel zum einlesen einer CSV Liste zu definieren:

    double_ >> *(double_ >> ',')
    

    Dies ist schon mal die standard Regel.

    Was meinst Du damit?

    phlox81 schrieb:

    Es gibt für Datentypen vordefinierte Regeln, hier double_. (Es gibt auch int_,char_, etc.) Jedoch speichert diese Regel noch keine Daten. Wenn wir die Werte in einen vector<double> ablegen wollen sieht dies so aus:

    double_[push_back(v,_1)] >> *(double_[push_back(v,_1)] >> ',')
    

    Das muss so aussehen:

    using boost::fusion::push_back;
    double_[push_back(v,_1)] >> *(',' >> double_[push_back(v,_1)])
    

    phlox81 schrieb:

    Hier ist push_back eine Funktion aus Fusion, welche das Ergebnis der double_ Regel in den Vector v einfügt. Die Klammern [] stellen wie in Spirit1.x den Block für die semantic Action.

    Es wäre sicher gut, darauf hinzuweisen, daß _1 ein Platzhalter für den Rückgabewert der Parserkomponente ist, an die die sematische Aktion angehängt ist.

    phlox81 schrieb:

    Wie in Spirit1.x gibt es auch in Spirit2 einen Listoperator, der solche Konstrukte verkürzt, und performanter ist, als obige Regel:

    double_[push_back(v,_1)] % ','
    

    Der Listoperator % ist nicht performanter, nur einfacher und übersichtlicher zu schreiben und zu verstehen.

    phlox81 schrieb:

    Diese Regel macht das Selbe, wie die Regel oben, jedoch in einer verkürzten Schreibweise.
    Spirit2 erlaubt es, für bestimmte Regeln Defaulttypen anzugeben, bzw. definiert für bestimmte Regeln schon Defaulttypen. Bei % ist dies ein std::vector + der Defaulttyp der vorranstehenden Regel (hier also ein std::vector<double>). Somit lässt sich nun auch ohne Fusion der Vector füllen, in dem man beim Aufruf der Parsingfunktion entsprechend den Vector v übergibt:

    bool r = phrase_parse(first, last,( double_ % ',' ),v, space);
    

    Du nimmst hier Bezug auf eine der entscheidenden Neuerungen in Spirit2. So ziemlich jede (in diesem Fall Parser-) Komponente hat einen bestimmten Rückgabe-Datentyp. Diese Komponenten generieren einen Wert (Instanz) dieses Datentyps wenn die entsprechende Eingabe erfolgreich geparst wurde. Für terminals (also primitive Komponenten), wie double_, ist dieser Datentyp fest definiert. Für non-terminals, wie rule's oder grammar's, muss dieser Datentyp explizit gegeben sein (siehe die unsigned() Notation oben).

    phlox81 schrieb:

    2.2.2 Grammatiken

    Wie schon in Spirit1.x gibt es auch in Spirit2 die Möglichkeit Regeldefinitionen in Grammatiken zusammenzufassen. Die Syntax dieser Grammatiken hat sich jedoch geändert. Während in Spirit1.x noch verschachteltes Definition Struct existierte, so erfolgt jetzt die Definition der Regeln in dem eigentlichen Grammatikkonstruktor.

    Ein Beispiel für Spirit2 Grammatikklassen:

    template <typename Iterator>
    struct roman : grammar_def<Iterator, unsigned()>
    {
        roman()
        {
            start
                =   +char_('M') [_val += 1000]
                ||  hundreds    [_val += _1]
                ||  tens        [_val += _1]
                ||  ones        [_val += _1];
    
            //  Note the use of the || operator. The expression
            //  a || b reads match a or b and in sequence. Try
            //  defining the roman numerals grammar in YACC or
            //  PCCTS. Spirit rules! :-)
        }
    
        rule<Iterator, unsigned()> start;
    };
    

    Die Grammatik wird als Template realisiert, damit sie auf unterschiedlichen Iteratoren anwendbar ist. Sie wird von einer entsprechenden Basisklasse (grammar_def<>) abgeleitet. Die beiden Parameter sind zum einen die Weiterleitung des Iteratortypen, und zum anderen die Definition des Defaulttypen dieser Grammatik.

    Es gibt noch einen 3. und einen 4. Template-Parameter: man kann lokale Variablen definieren, die im scope der jeweiligen Grammatikinstanz definiert sind und die Spirit1 closure-Variablen ersetzen. Und man kann einen skip-Parser angeben, der von dieser Grammatik verwendet wird. Die Syntax für die lokalen Varablen ist zum Beispiel

    locals<int, double>
    

    welches 2 lokale Variablen für jede Instanz dieser Grammatik definiert, eine vom Typ int und eine vom Typ double. Der Nutzer kann auf diese durch Verwendung spezieller Platzhalter zugreifen: _a, _b, _c usw.

    phlox81 schrieb:

    Im Konstruktor wird nun die Startregel definiert.

    start ist lediglich der Name der Regel, die standardmäßig als Startregel verwendet wird. Man kann jede andere Regel als Startregel verwenden in dem man diese als 2. Konstruktor-Parameter für die grammar verwendet.
    Es ist sicher sinnvoll, auch die Verwendung einer Grammatik zu beschreiben:

    roman<iterator_type> def; //  Our grammar definition
        grammar<roman> roman_parser(def); // Our grammar
        unsigned result;
        bool r = parse(iter, end, roman_parser[ref(result) = _1]);
    

    phlox81 schrieb:

    Der || operator stellt in Spirit ein sequenzielles Oder dar, so das hier jede Regel ausgewertet wird, wenn sie erfolgreich angewendet wird, oder bei keinem Match die darauffolgende Regel. _val stellt die Defaultwert der Grammatik da, quasi ihren Rückgabewert. Dieser wird hier einfach mit dem Ergebnis der Regel addiert. Die Grammatik soll so eine römische in eine arabische Zahl konvertieren. Am Ende wird dann noch die Startregel definiert. Richtig, es fehlt die Definition der anderen Regeln. Wie man die Regeln eines Lexers in Spirit auch in einer Grammatik verwenden kann, so ist es möglich auch andere Elemente als Regeln zu verwenden. Hier sind dies Symboltabellen, in dem Symbole aus dem Eingabestrom mit festen Werten verbunden werden.

    Und so sieht dann eine Symboltabelle in Spirit2 aus:

    struct ones_ : symbols<char, unsigned>
    {
        ones_()
        {
            add
                ("I"    , 1)
                ("II"   , 2)
                ("III"  , 3)
                ("IV"   , 4)
                ("V"    , 5)
                ("VI"   , 6)
                ("VII"  , 7)
                ("VIII" , 8)
                ("IX"   , 9)
            ;
        }
    
    } ones;
    

    Auch hier wird ein Struct definiert, aber diesmal von symbols abgeleitet, mit den Typen char und unsingend. Welche die "Spalten" in der Tabelle darstellen. Im Konstruktor wird nun mit der Funktion add die Tabelle mit Symbolen gefüllt. Welche dann später bei der Regelanwendung mit dem Eingabestrom verglichen werden. Wobei das Symbol bevorzugt wird, was den meisten Input abdeckt (siehe "I,II,III").

    • Willst Du die anderen Symboltabellen auch noch zitieren ( tens und hundrets )?
    • Vielleicht solltest Du noch beschreiben, welche Bedeutung die Template-Parameter char und unsigned haben?

    phlox81 schrieb:

    2.2.3 Bäume in Spirit2

    Es gibt die Möglichkeit komplette Datenstrukturen von Spirit einlesen zu lassen, z.B. als Baum. Und um z.b. XML parsen zu können, benötigt man auch zugriff auf das vorherige Ergebnis des Parsers, um z.b. einen Endtag zum richtigen Starttag zu matchen. Ebenso können rekursive Regeln definiert werden, welche sich selbst oder sich wieder über dritte Regeln einbinden.
    Als Datenstruktur für Bäume lässt sich boost::variant nutzen, wie dieses Beispiel verdeutlicht:

    struct mini_xml;
    
    typedef
        boost::variant<
            boost::recursive_wrapper<mini_xml>
          , std::string
        >
    mini_xml_node;
    
    struct mini_xml
    {
        std::string name;                           // tag name
        std::vector<mini_xml_node> children;        // children
    };
    
    // We need to tell fusion about our mini_xml struct
    // to make it a first-class fusion citizen
    BOOST_FUSION_ADAPT_STRUCT(
        mini_xml,
        (std::string, name)
        (std::vector<mini_xml_node>, children)
    )
    

    Fusion wird hier benutzt, um Zugriff auf die einzelnen Elemente des Structs zu haben.
    Die entsprechende Grammatik sieht dann so aus:

    template <typename Iterator>
    struct mini_xml_def : grammar_def<Iterator, mini_xml(), space_type>
    {
        mini_xml_def()
        {
            text = lexeme[+(char_ - '<')[_val += _1]];
            node = (xml | text)[_val = _1];
    
            start_tag =
                    '<'
                >>  lexeme[+(char_ - '>')[_val += _1]]
                >>  '>'
            ;
    
            end_tag =
                    "</"
                >>  lit(_r1)
                >>  '>'
            ;
    
            xml =
                    start_tag[at_c<0>(_val) = _1]
                >>  *node[push_back(at_c<1>(_val), _1)]
                >>  end_tag(at_c<0>(_val))
            ;
        }
    
        rule<Iterator, mini_xml(), space_type> xml;
        rule<Iterator, mini_xml_node(), space_type> node;
        rule<Iterator, std::string(), space_type> text;
        rule<Iterator, std::string(), space_type> start_tag;
        rule<Iterator, void(std::string), space_type> end_tag;
    };
    

    Dies ist zunächst eine ganz normale Grammatik, wie sie schon im Kapitel vorher vorgestellt wurde. Der Rückgabetyp ist mini_xml und space_type ist der entsprechende Skipparser. Danach erfolgt die Definition der Regeln:

    • text
      Hier wird eine einfache Regel für das einlesen von zusammenhängendem Text definiert. Dabei stellt lexeme sicher das nur zusammenhängende Buchstabenfolgen eingelesen werden, nach einem Leerzeichen wird die nächste Regel angewendet. Mit _val wird hier auf den jeweiligen Defaulttypen bzw. Defaultwert der Regel zugegriffen, um das Ergebnis zu speichern.
    • node
      In dieser Regel wird nur Definiert, das entweder die Regel "xml" oder die Regel "text" anzuwenden ist.
    • start_tag
      Definiert einen (Pseudo) XML-Starttag als '<' gefolgt von einer zusammenhängenden Buchstabensequenz und abgeschlossen von '>'.
    • end_tag
      Definiert den Endtag des (Pseudo) XML-Formats. Wobei _r1 hier ein übergebnes Argument ist, welches mit der Funktion lit in eine Zeichenkette umgewandelt wird.
    • xml
      Dies ist die Haupt oder Startregel des Mini-XML Parsers. Sie definiert das MiniXML ein Starttag gefolgt von 0 oder beliebig vielen node-Regeln mit einem passenden Endtag ist. Im Detail wird hier über Fusion auf das Struct mini_xml zugegriffen. Nach der start_tag Regel wird das Ergebnis an die erste Variable des Structs übergeben, den Namen des Elementes. Danach wird die Sequenz der Noderegeln an die 2. Variable übergeben. Welches die Kinder dieses Elementes sind. Zum Schluss wird dann noch die end_tag Regel aufgerufen und ihr wird der Name des dazugehörigen Starttags übergeben. Somit kann end_tag feststellen ob das korrekte Endtag vorliegt.

    Auch wenn der mini-XML parser einen kompletten Überblick über so ziemlich alle features von Spirit2 gibt, denke ich, daß es ein recht komplexes Beispiel ist, welches mit den jetzigen Erläuterungen Gefahr läuft, unverständlich zu bleiben. Entweder Du fügst noch viel mehr Erklärungen hinzu oder solltest ein einfacheres Beispiel wählen. Das Erzeugen von Bäumen in Spirit2 hängt sehr eng mit den Rückgabewerten der Parserkomponenten zusammen. Eine Beschreibung einiger dieser Zusammenhänge hilft vielleicht.

    phlox81 schrieb:

    2.3 spirit::karma

    Dieser Namensraum ist neu. Es handelt sich hier um das Gegenstück zu spirit::qi, welches für die Ausgabe von Daten in einen Ausgabestrom zuständig ist. Karma nutzt hierfür den << operator, analog zu qi, welches den >> operator für die Inputsequenz benutzt.
    Karma ist daher auch vom Aufbau recht ähnlich zu qi, nur das es halt keine Inputregeln sondern Ausgaberegeln definiert. Das letzte Beispiel aus spirit::qi, MiniXML, könnte man ja auch wieder in eine Datei schreiben wollen, um z.B. ein XML Format in ein anderes zu wandeln. Mit Karma ist nun genau dies möglich:

    template <typename OutputIterator>
    struct mini_xml_generator
      : boost::spirit::karma::grammar_def<OutputIterator, void(mini_xml), space_type>
    {
     typedef karma::grammar_def<OutputIterator, void(mini_xml), space_type> base_type;
     boost::mpl::print<typename base_type::start_type::param_types> x;
    
        mini_xml_generator()
        {
                 text = verbatim[lit(text._1)];
                 node = (xml | text)[_1 = node._1];
    
                 start_tag =
                         '<'
                     <<  verbatim[lit(start_tag._1)]
                     <<  '>'
                 ;
    
                 end_tag =
                         "</"
                     <<  verbatim[lit(end_tag._1)]
                     <<  '>'
                 ;
    
             xml =
                     start_tag(at_c<0>(xml._1))
                     <<  (*node)[ref(at_c<1>(xml._1))]
                 <<  end_tag(at_c<0>(xml._1))
            ;
        }
    
        karma::rule<OutputIterator, void(mini_xml), space_type> xml;
        karma::rule<OutputIterator, void(mini_xml_node), space_type> node;
        karma::rule<OutputIterator, void(std::string), space_type> text;
        karma::rule<OutputIterator, void(std::string), space_type> start_tag;
        karma::rule<OutputIterator, void(std::string), space_type> end_tag;
    };
    

    Auch hier wird die Ausgabeklasse von einer entsprechenden Basisklasse abgeleitet. Ebenfalls werden ähnliche Regeln wie in der qi Grammatik angewandt. Die Direktive verbatim ist das Gegenstück zu lexeme in qi, verbatim stellt sicher, das keine Leerzeichen im Text sind.

    Richtiger wäre zu sagen: verbatim[] stellt sicher, daß keine zusätzlichen Trennzeichen in die Ausgabe eingefügt werden. Ähnlich wie Spirit.Qi es erlaubt, einen skip-Parser zu verwenden, der bestimmte Zeichen in der Eingabe ignoriert (und man dieses Verhalten mit lexeme[] unterbinden kann), so ermöglicht Spirit.Karma delimit-Generatoren vorzugeben, die zusätzliche Trennsymbole in der Ausgabe einfügen.

    phlox81 schrieb:

    Die korrekten Werte für die Ausgabe werden von der Hauptregel XML dann auch an die eingehängten Regeln wie z.B. start_tag weitergereicht. Mit Regelname._1 greift man jeweils auf diesen Wert zu.

    Das hat sich geändert. Die richtige Syntax ist hier (genau wie in Qi): regelname._r1 etc.

    phlox81 schrieb:

    3 Infos zu Spirit2

    Spirit2 wurde in diesem Frühjahr auf der Boostkonference in Aspen, Colorado vorgestellt. Es ist soweit lauffähig und einsatzbereit. Allerdings gibt es noch keine offizielle Dokumentation, wie wir sie z.B. von spirit1.x kennen. Es existiert ein SVN Verzeichnis, welches auch Examples und einige Präsentationen enthält. Daran habe ich mich auch für meinen Vortrag auf dem Treffen und diesen Artikel orientiert. Wer nicht die Möglichkeit hat, Spirit2 aus dem SVN Verzeichnis zu beziehen, kann sich hier auch den Snapshot von der Boostkonferenz herunterladen.
    Spirit2 hat einige Abhängigkeiten zum aktuellen cvs::HEAD von Boost, somit benötigt man einen aktuellen Checkout von Boost um Spirit2 zu kompilieren. Dabei geht man so vor, das man in einem Verzeichnis den aktuellen Checkout und in einem anderen Spirit liegen hat, bei den Pfadangaben des Compilers/der IDE muss man nun dafür sorgen, das der Compiler zuerst das spirit2-Verzeichnis findet, und dann erst das boost-Verzeichnis.

    Vielleicht solltest Du noch einen Hinweis hinzufügen, der die verwendbaren Compiler beschreibt.

    Alles in allem würde ich gern mit Dir an diesem Artikel zusammenarbeiten. Ich habe hier auch schon einiges Material gesammelt und würde gern Doppelarbeiten vermeiden.

    Regards Hartmut



  • Hm, danke für das Feedback.
    Ich zitiere jetzt mal nicht vollständig, weil sonst wird es unübersichtlich.

    hkaiser schrieb:

    Als Vorbemerkung: ich finde es cool, daß bereits jetzt, noch bevor Spirit2 released ist und auch noch kaum Dokumentation vorhanden ist, Artikel darüber geschrieben werden. Gleichzeitig hat das natürlich zur Folge, daß immer noch Änderungen an Syntax und Semantik erfolgen und daher diese Artikel u.U. nicht immer genau beschreiben, wie Spirit2 wirklich aussehen wird. In diesem Sinne seien mir einige Bemerkungen erlaubt... Der Einfachheit halber habe ich einfach den Artikel vollständig zitiert und meine Kommentare dazwischengemogelt. Ich bin mir nicht sicher wie es bei Euch üblich ist, Artikelentwürfe zu kommentieren, seht es mir bitte nach, wenn ich gegen die Konventionen vertoße.

    Ist ok. Allerdings lösche bitte dinge, die nicht benötigt werden, der übersicht halber.
    Wenn der Release von Spirit2 noch nicht da ist, wann wird er kommen?
    Dachte das wäre mit der Boostcon geschehen...

    hkaiser schrieb:

    phlox81 schrieb:

    Die wohl größte Neuerung ist, das Spirit2 nun über drei neue Toplevel Namespaces verfügt:

    • spirit::lex - ein Modul für die Definition von Lexern
    • spirit::qi - Das Modul für die Definition von Regeln zum parsen von Input
    • spirit::karma - Ein Modul zum Formatieren der Ausgabe

    Auf die Details zu diesen Modulen gehe ich später noch genauer ein.
    Neben diesen neuen Modulen, hat sich auch einiges an der Syntax von Grammatiken und anderen spirit Strukturen getan.
    So ist das gekapselte definition Struct von spirit1.x jetzt nicht mehr in der Grammatikklasse nötig:

    spirit1.x Code:

    struct testgrammar : public boost::spirit::grammar<testgrammar>
    	{
    		template<class ScannerT>
    		struct definition
    		{
    			rule<ScannerT> rrule;
    			
    			definition(testgrammar const& self)
    			{
    				rrule =  /* definition */
    			}
    			rule<ScannerT> start() const
    			{ return rrule; }
    
    		};
    	};
    

    spirit2.0 Code:

    template <typename Iterator>
    struct testgrammar : grammar_def<Iterator, unsigned()>
    // der 2. Templateparameter ist der "Rückgabewert" der Grammatik
    {
        testgrammar()
        {
            start = /* definition */
        }
        // der 2. Templateparameter ist der "Rückgabewert" der Grammatik
        rule<Iterator, unsigned()> start;
    };
    
    • Aus meiner Sicht wäre es besser, die alte Syntax an dieser Stelle nicht zu zitieren, da dies verwirrend sein kann.
    • Der 2. template Parameter ist nicht nur der Rückgabewert, sondern kodiert (ähnlich wie bei den rule's) sowohl die 'Parameter' (inherited attributes) als auch den 'Rückgabewert' (synthesized attribute) der Grammatik. Um die Notation soweit wie möglich an die logische Bedeutung eines non-terminals anzupassen, haben wir die Funktions-Notation gewählt, hier unsigned(), also ein non-terminal, welche keine Parameter erwarted, aber einen unsigned Wert generiert

    Hm, wäre evtl. überlegenswert es als Code zu streichen, da ja später noch Grammatiken kommen.

    hkaiser schrieb:

    phlox81 schrieb:

    2 Drei Beispiele für lex, qi & karma

    Um einen Überblick über die neuen Bereiche in Spirit zu bekommen, ist es am besten sich einige Beispiele selber anzuschauen.
    Leider gibt es noch keine Onlinedokumentation oder ähnliches, aus dem man mehr Informationen ziehen kann.
    Aber es gibt die Möglichkeit im Example Verzeichnis zu lesen.

    2.1 spirit::lex

    Lex ist der neue Bereich für Lexer in Spirit. Es gibt die Möglichkeit auch andere Lexer in Spirit zu verwenden, als den Standardlexer von Spirit. Lex dient dazu, den Zeichenstrom in Tokens auf zu teilen, so das die eigentlichen Regeln des Parsers auf den Token des Lexers und nicht mit dem Eingabestrom arbeitet.
    Dies ist allerdings optional, es besteht in Spirit2.0 kein Zwang einen Lexer zu benutzen.

    ...

    Die Lexerkomponente von Spirit2 ist eine der neuen Bestandteile, welche sich auf jeden Fall noch verändern werden. So wird es möglich sein, explizite lexer states direkt aus dem Parsevorgang heraus zu steuern. Das wird zur Folge haben, daß die Definition und Nutzung dieser lexer states anders sein wird als oben beschrieben, was widerum Auswirkungen auf die Nutzung von Tokens als Skipparser haben dürfte.
    Die entgültige Syntax für die Lexerkomponente steht noch nicht fest, ich bin mir daher nicht sicher ob es gut ist, die Lexer Komponente überhaupt zu beschreiben.

    Habe auch überlegt, wie detailiert es sein soll. Würde beführworten den Bereich zu erwähnen, aber die Hintergrundinfos sind mir neu. Kann ich einfliessen lassen.
    Den letzten Codeblock würde ich dann streichen.

    hkaiser schrieb:

    phlox81 schrieb:

    2.2 spirit::qi

    Mit spirit::qi gibt es nun auch einen eigenständigen Bereich für die Textverarbeitung (parsen). Es gibt einige Veränderungen in diesem Bereich, der quasi auf die Erfahrungen von Spirit1.x aufsetzt. So haben sich die Grammatiken verändert und einige Operatoren sind neu, Sowie eine neue Möglichkeit für die Fehlerbehandlung.

    Spirit.Karma ist auch Textverarbeitung...
    Richtig ist aber, daß faktisch die gesamte Funktionalität, die Spirit1 bisher ausgemacht hat, nun im namespace boost::spirit::qi implementiert wurde.

    Stimmt. Werd ich noch was besser formulieren.

    hkaiser schrieb:

    phlox81 schrieb:

    2.2.1 Regeldefinitionen

    So gibt es nun mehrere Möglichkeiten, die Regel zum einlesen einer CSV Liste zu definieren:

    double_ >> *(double_ >> ',')
    

    Dies ist schon mal die standard Regel.

    Was meinst Du damit?

    Gute Frage *g* Es ist quasi die standard Spirit-Lösung für dieses Problem.

    hkaiser schrieb:

    phlox81 schrieb:

    Es gibt für Datentypen vordefinierte Regeln, hier double_. (Es gibt auch int_,char_, etc.) Jedoch speichert diese Regel noch keine Daten. Wenn wir die Werte in einen vector<double> ablegen wollen sieht dies so aus:

    double_[push_back(v,_1)] >> *(double_[push_back(v,_1)] >> ',')
    

    Das muss so aussehen:

    using boost::fusion::push_back;
    double_[push_back(v,_1)] >> *(',' >> double_[push_back(v,_1)])
    

    Ja, wäre ein hilfreicher Zusatz.

    hkaiser schrieb:

    phlox81 schrieb:

    Hier ist push_back eine Funktion aus Fusion, welche das Ergebnis der double_ Regel in den Vector v einfügt. Die Klammern [] stellen wie in Spirit1.x den Block für die semantic Action.

    Es wäre sicher gut, darauf hinzuweisen, daß _1 ein Platzhalter für den Rückgabewert der Parserkomponente ist, an die die sematische Aktion angehängt ist.

    phlox81 schrieb:

    Wie in Spirit1.x gibt es auch in Spirit2 einen Listoperator, der solche Konstrukte verkürzt, und performanter ist, als obige Regel:

    double_[push_back(v,_1)] % ','
    

    Der Listoperator % ist nicht performanter, nur einfacher und übersichtlicher zu schreiben und zu verstehen.

    Doch ist er. Hab ich Messungen zu gemacht. Hab dir den entsprechenden Testfall auch zugeschickt, und es ist definitiv schneller mit %.

    hkaiser schrieb:

    phlox81 schrieb:

    Diese Regel macht das Selbe, wie die Regel oben, jedoch in einer verkürzten Schreibweise.
    Spirit2 erlaubt es, für bestimmte Regeln Defaulttypen anzugeben, bzw. definiert für bestimmte Regeln schon Defaulttypen. Bei % ist dies ein std::vector + der Defaulttyp der vorranstehenden Regel (hier also ein std::vector<double>). Somit lässt sich nun auch ohne Fusion der Vector füllen, in dem man beim Aufruf der Parsingfunktion entsprechend den Vector v übergibt:

    bool r = phrase_parse(first, last,( double_ % ',' ),v, space);
    

    Du nimmst hier Bezug auf eine der entscheidenden Neuerungen in Spirit2. So ziemlich jede (in diesem Fall Parser-) Komponente hat einen bestimmten Rückgabe-Datentyp. Diese Komponenten generieren einen Wert (Instanz) dieses Datentyps wenn die entsprechende Eingabe erfolgreich geparst wurde. Für terminals (also primitive Komponenten), wie double_, ist dieser Datentyp fest definiert. Für non-terminals, wie rule's oder grammar's, muss dieser Datentyp explizit gegeben sein (siehe die unsigned() Notation oben).

    k.

    hkaiser schrieb:

    phlox81 schrieb:

    2.2.2 Grammatiken

    Wie schon in Spirit1.x gibt es auch in Spirit2 die Möglichkeit Regeldefinitionen in Grammatiken zusammenzufassen. Die Syntax dieser Grammatiken hat sich jedoch geändert. Während in Spirit1.x noch verschachteltes Definition Struct existierte, so erfolgt jetzt die Definition der Regeln in dem eigentlichen Grammatikkonstruktor.

    Ein Beispiel für Spirit2 Grammatikklassen:

    template <typename Iterator>
    struct roman : grammar_def<Iterator, unsigned()>
    {
        roman()
        {
            start
                =   +char_('M') [_val += 1000]
                ||  hundreds    [_val += _1]
                ||  tens        [_val += _1]
                ||  ones        [_val += _1];
    
            //  Note the use of the || operator. The expression
            //  a || b reads match a or b and in sequence. Try
            //  defining the roman numerals grammar in YACC or
            //  PCCTS. Spirit rules! :-)
        }
    
        rule<Iterator, unsigned()> start;
    };
    

    Die Grammatik wird als Template realisiert, damit sie auf unterschiedlichen Iteratoren anwendbar ist. Sie wird von einer entsprechenden Basisklasse (grammar_def<>) abgeleitet. Die beiden Parameter sind zum einen die Weiterleitung des Iteratortypen, und zum anderen die Definition des Defaulttypen dieser Grammatik.

    Es gibt noch einen 3. und einen 4. Template-Parameter: man kann lokale Variablen definieren, die im scope der jeweiligen Grammatikinstanz definiert sind und die Spirit1 closure-Variablen ersetzen. Und man kann einen skip-Parser angeben, der von dieser Grammatik verwendet wird. Die Syntax für die lokalen Varablen ist zum Beispiel

    locals<int, double>
    

    welches 2 lokale Variablen für jede Instanz dieser Grammatik definiert, eine vom Typ int und eine vom Typ double. Der Nutzer kann auf diese durch Verwendung spezieller Platzhalter zugreifen: _a, _b, _c usw.

    Ja, wäre erwähnenswert. Ist halt nirgendwo dokumentiert, so wusste ich das nicht 100% 😉

    hkaiser schrieb:

    phlox81 schrieb:

    Im Konstruktor wird nun die Startregel definiert.

    start ist lediglich der Name der Regel, die standardmäßig als Startregel verwendet wird. Man kann jede andere Regel als Startregel verwenden in dem man diese als 2. Konstruktor-Parameter für die grammar verwendet.
    Es ist sicher sinnvoll, auch die Verwendung einer Grammatik zu beschreiben:

    roman<iterator_type> def; //  Our grammar definition
        grammar<roman> roman_parser(def); // Our grammar
        unsigned result;
        bool r = parse(iter, end, roman_parser[ref(result) = _1]);
    

    Gute Idee.

    hkaiser schrieb:

    phlox81 schrieb:

    Der || operator stellt in Spirit ein sequenzielles Oder dar, so das hier jede Regel ausgewertet wird, wenn sie erfolgreich angewendet wird, oder bei keinem Match die darauffolgende Regel. _val stellt die Defaultwert der Grammatik da, quasi ihren Rückgabewert. Dieser wird hier einfach mit dem Ergebnis der Regel addiert. Die Grammatik soll so eine römische in eine arabische Zahl konvertieren. Am Ende wird dann noch die Startregel definiert. Richtig, es fehlt die Definition der anderen Regeln. Wie man die Regeln eines Lexers in Spirit auch in einer Grammatik verwenden kann, so ist es möglich auch andere Elemente als Regeln zu verwenden. Hier sind dies Symboltabellen, in dem Symbole aus dem Eingabestrom mit festen Werten verbunden werden.

    Und so sieht dann eine Symboltabelle in Spirit2 aus:

    struct ones_ : symbols<char, unsigned>
    {
        ones_()
        {
            add
                ("I"    , 1)
                ("II"   , 2)
                ("III"  , 3)
                ("IV"   , 4)
                ("V"    , 5)
                ("VI"   , 6)
                ("VII"  , 7)
                ("VIII" , 8)
                ("IX"   , 9)
            ;
        }
    
    } ones;
    

    Auch hier wird ein Struct definiert, aber diesmal von symbols abgeleitet, mit den Typen char und unsingend. Welche die "Spalten" in der Tabelle darstellen. Im Konstruktor wird nun mit der Funktion add die Tabelle mit Symbolen gefüllt. Welche dann später bei der Regelanwendung mit dem Eingabestrom verglichen werden. Wobei das Symbol bevorzugt wird, was den meisten Input abdeckt (siehe "I,II,III").

    • Willst Du die anderen Symboltabellen auch noch zitieren ( tens und hundrets )?
    • Vielleicht solltest Du noch beschreiben, welche Bedeutung die Template-Parameter char und unsigned haben?

    Nein, will ich nicht, es soll nur der Symboltable vorgestellt werden. Aber nicht das ganze beispiel voll zitiert werden. Der Artikel soll ja nur einen Überlick liefern.

    hkaiser schrieb:

    phlox81 schrieb:

    2.2.3 Bäume in Spirit2

    Es gibt die Möglichkeit komplette Datenstrukturen von Spirit einlesen zu lassen, z.B. als Baum. Und um z.b. XML parsen zu können, benötigt man auch zugriff auf das vorherige Ergebnis des Parsers, um z.b. einen Endtag zum richtigen Starttag zu matchen. Ebenso können rekursive Regeln definiert werden, welche sich selbst oder sich wieder über dritte Regeln einbinden.
    Als Datenstruktur für Bäume lässt sich boost::variant nutzen, wie dieses Beispiel verdeutlicht:
    ...
    Dies ist die Haupt oder Startregel des Mini-XML Parsers. Sie definiert das MiniXML ein Starttag gefolgt von 0 oder beliebig vielen node-Regeln mit einem passenden Endtag ist. Im Detail wird hier über Fusion auf das Struct mini_xml zugegriffen. Nach der start_tag Regel wird das Ergebnis an die erste Variable des Structs übergeben, den Namen des Elementes. Danach wird die Sequenz der Noderegeln an die 2. Variable übergeben. Welches die Kinder dieses Elementes sind. Zum Schluss wird dann noch die end_tag Regel aufgerufen und ihr wird der Name des dazugehörigen Starttags übergeben. Somit kann end_tag feststellen ob das korrekte Endtag vorliegt.
    [/list]

    Auch wenn der mini-XML parser einen kompletten Überblick über so ziemlich alle features von Spirit2 gibt, denke ich, daß es ein recht komplexes Beispiel ist, welches mit den jetzigen Erläuterungen Gefahr läuft, unverständlich zu bleiben. Entweder Du fügst noch viel mehr Erklärungen hinzu oder solltest ein einfacheres Beispiel wählen. Das Erzeugen von Bäumen in Spirit2 hängt sehr eng mit den Rückgabewerten der Parserkomponenten zusammen. Eine Beschreibung einiger dieser Zusammenhänge hilft vielleicht.

    Hab ich auch überlegt, evtl. sinnvoller daraus später einen eigenen Artikel zu machen.

    hkaiser schrieb:

    phlox81 schrieb:

    2.3 spirit::karma

    Dieser Namensraum ist neu. Es handelt sich hier um das Gegenstück zu spirit::qi, welches für die Ausgabe von Daten in einen Ausgabestrom zuständig ist. Karma nutzt hierfür den << operator, analog zu qi, welches den >> operator für die Inputsequenz benutzt.
    Karma ist daher auch vom Aufbau recht ähnlich zu qi, nur das es halt keine Inputregeln sondern Ausgaberegeln definiert. Das letzte Beispiel aus spirit::qi, MiniXML, könnte man ja auch wieder in eine Datei schreiben wollen, um z.B. ein XML Format in ein anderes zu wandeln. Mit Karma ist nun genau dies möglich:

    template <typename OutputIterator>
    struct mini_xml_generator
      : boost::spirit::karma::grammar_def<OutputIterator, void(mini_xml), space_type>
    {
     typedef karma::grammar_def<OutputIterator, void(mini_xml), space_type> base_type;
     boost::mpl::print<typename base_type::start_type::param_types> x;
    
        mini_xml_generator()
        {
                 text = verbatim[lit(text._1)];
                 node = (xml | text)[_1 = node._1];
    
                 start_tag =
                         '<'
                     <<  verbatim[lit(start_tag._1)]
                     <<  '>'
                 ;
    
                 end_tag =
                         "</"
                     <<  verbatim[lit(end_tag._1)]
                     <<  '>'
                 ;
    
             xml =
                     start_tag(at_c<0>(xml._1))
                     <<  (*node)[ref(at_c<1>(xml._1))]
                 <<  end_tag(at_c<0>(xml._1))
            ;
        }
    
        karma::rule<OutputIterator, void(mini_xml), space_type> xml;
        karma::rule<OutputIterator, void(mini_xml_node), space_type> node;
        karma::rule<OutputIterator, void(std::string), space_type> text;
        karma::rule<OutputIterator, void(std::string), space_type> start_tag;
        karma::rule<OutputIterator, void(std::string), space_type> end_tag;
    };
    

    Auch hier wird die Ausgabeklasse von einer entsprechenden Basisklasse abgeleitet. Ebenfalls werden ähnliche Regeln wie in der qi Grammatik angewandt. Die Direktive verbatim ist das Gegenstück zu lexeme in qi, verbatim stellt sicher, das keine Leerzeichen im Text sind.

    Richtiger wäre zu sagen: verbatim[] stellt sicher, daß keine zusätzlichen Trennzeichen in die Ausgabe eingefügt werden. Ähnlich wie Spirit.Qi es erlaubt, einen skip-Parser zu verwenden, der bestimmte Zeichen in der Eingabe ignoriert (und man dieses Verhalten mit lexeme[] unterbinden kann), so ermöglicht Spirit.Karma delimit-Generatoren vorzugeben, die zusätzliche Trennsymbole in der Ausgabe einfügen.

    phlox81 schrieb:

    Die korrekten Werte für die Ausgabe werden von der Hauptregel XML dann auch an die eingehängten Regeln wie z.B. start_tag weitergereicht. Mit Regelname._1 greift man jeweils auf diesen Wert zu.

    Das hat sich geändert. Die richtige Syntax ist hier (genau wie in Qi): regelname._r1 etc.

    k. gut zu wissen. Steht das auch in den Examples?

    hkaiser schrieb:

    phlox81 schrieb:

    3 Infos zu Spirit2

    Spirit2 wurde in diesem Frühjahr auf der Boostkonference in Aspen, Colorado vorgestellt. Es ist soweit lauffähig und einsatzbereit. Allerdings gibt es noch keine offizielle Dokumentation, wie wir sie z.B. von spirit1.x kennen. Es existiert ein SVN Verzeichnis, welches auch Examples und einige Präsentationen enthält. Daran habe ich mich auch für meinen Vortrag auf dem Treffen und diesen Artikel orientiert. Wer nicht die Möglichkeit hat, Spirit2 aus dem SVN Verzeichnis zu beziehen, kann sich hier auch den Snapshot von der Boostkonferenz herunterladen.
    Spirit2 hat einige Abhängigkeiten zum aktuellen cvs::HEAD von Boost, somit benötigt man einen aktuellen Checkout von Boost um Spirit2 zu kompilieren. Dabei geht man so vor, das man in einem Verzeichnis den aktuellen Checkout und in einem anderen Spirit liegen hat, bei den Pfadangaben des Compilers/der IDE muss man nun dafür sorgen, das der Compiler zuerst das spirit2-Verzeichnis findet, und dann erst das boost-Verzeichnis.

    Vielleicht solltest Du noch einen Hinweis hinzufügen, der die verwendbaren Compiler beschreibt.

    Alles in allem würde ich gern mit Dir an diesem Artikel zusammenarbeiten. Ich habe hier auch schon einiges Material gesammelt und würde gern Doppelarbeiten vermeiden.

    Regards Hartmut

    Ja, da müsste ich halt wissen welche das sind. Werde den Artikel morgen mal überarbeiten, bin noch offen für Vorschläge und Anregungen 🙂

    phlox



  • phlox81 schrieb:

    hkaiser schrieb:

    Als Vorbemerkung: ich finde es cool, daß bereits jetzt, noch bevor Spirit2 released ist und auch noch kaum Dokumentation vorhanden ist, Artikel darüber geschrieben werden. Gleichzeitig hat das natürlich zur Folge, daß immer noch Änderungen an Syntax und Semantik erfolgen und daher diese Artikel u.U. nicht immer genau beschreiben, wie Spirit2 wirklich aussehen wird. In diesem Sinne seien mir einige Bemerkungen erlaubt... Der Einfachheit halber habe ich einfach den Artikel vollständig zitiert und meine Kommentare dazwischengemogelt. Ich bin mir nicht sicher wie es bei Euch üblich ist, Artikelentwürfe zu kommentieren, seht es mir bitte nach, wenn ich gegen die Konventionen vertoße.

    Ist ok. Allerdings lösche bitte dinge, die nicht benötigt werden, der übersicht halber.

    Ok, verstanden.

    phlox81 schrieb:

    Wenn der Release von Spirit2 noch nicht da ist, wann wird er kommen?
    Dachte das wäre mit der Boostcon geschehen...

    Wir haben Spirit2 offiziell auf der BoostCon vorgestellt. Den Release selbst können wir aber erst machen, wenn die Dokumentation fertig ist. Wir arbeiten dran, ich kann aber noch keinen Termin nennen. Wir orientieren auf Ende des Jahres, werden aber Zwischenschritte veröffentlichen.

    phlox81 schrieb:

    hkaiser schrieb:

    • Aus meiner Sicht wäre es besser, die alte Syntax an dieser Stelle nicht zu zitieren, da dies verwirrend sein kann.
    • Der 2. template Parameter ist nicht nur der Rückgabewert, sondern kodiert (ähnlich wie bei den rule's) sowohl die 'Parameter' (inherited attributes) als auch den 'Rückgabewert' (synthesized attribute) der Grammatik. Um die Notation soweit wie möglich an die logische Bedeutung eines non-terminals anzupassen, haben wir die Funktions-Notation gewählt, hier unsigned(), also ein non-terminal, welche keine Parameter erwarted, aber einen unsigned Wert generiert

    Hm, wäre evtl. überlegenswert es als Code zu streichen, da ja später noch Grammatiken kommen.

    Ok, wichtig ist aber aus meiner Sicht auf die Funktions-Notation der Parameter und Rückgabewerte hinzuweisen. Das ist wichtig fürs Verständnis, weil es eine zentral Bedeutung für alles in Spirit hat (nicht nur Qi, sondern auch für Karma).

    phlox81 schrieb:

    hkaiser schrieb:

    Die Lexerkomponente von Spirit2 ist eine der neuen Bestandteile, welche sich auf jeden Fall noch verändern werden. So wird es möglich sein, explizite lexer states direkt aus dem Parsevorgang heraus zu steuern. Das wird zur Folge haben, daß die Definition und Nutzung dieser lexer states anders sein wird als oben beschrieben, was widerum Auswirkungen auf die Nutzung von Tokens als Skipparser haben dürfte.
    Die entgültige Syntax für die Lexerkomponente steht noch nicht fest, ich bin mir daher nicht sicher ob es gut ist, die Lexer Komponente überhaupt zu beschreiben.

    Habe auch überlegt, wie detailiert es sein soll. Würde befürworten den Bereich zu erwähnen, aber die Hintergrundinfos sind mir neu. Kann ich einfliessen lassen.
    Den letzten Codeblock würde ich dann streichen.

    Ok, klingt gut. Ja, die erwähnten Änderungen am Lexer sind das Ergebniss einiger Diskussionen, die wir mit verschiedenen Leuten während der BoostCon hatten. Die genaue Syntax steht aber wie gesagt noch nicht fest, ich bin aber gerade dabei es zu implementieren.

    phlox81 schrieb:

    hkaiser schrieb:

    phlox81 schrieb:

    Es gibt für Datentypen vordefinierte Regeln, hier double_. (Es gibt auch int_,char_, etc.) Jedoch speichert diese Regel noch keine Daten. Wenn wir die Werte in einen vector<double> ablegen wollen sieht dies so aus:

    double_[push_back(v,_1)] >> *(double_[push_back(v,_1)] >> ',')
    

    Das muss so aussehen:

    using boost::fusion::push_back;
    double_[push_back(v,_1)] >> *(',' >> double_[push_back(v,_1)])
    

    Ja, wäre ein hilfreicher Zusatz.

    Ich meinte nicht nur den Zusatz. Du hast die rechte Seite des Ausdrucks verdreht: erst das Komma, dann das double_.

    phlox81 schrieb:

    hkaiser schrieb:

    phlox81 schrieb:

    Wie in Spirit1.x gibt es auch in Spirit2 einen Listoperator, der solche Konstrukte verkürzt, und performanter ist, als obige Regel:

    double_[push_back(v,_1)] % ','
    

    Der Listoperator % ist nicht performanter, nur einfacher und übersichtlicher zu schreiben und zu verstehen.

    Doch ist er. Hab ich Messungen zu gemacht. Hab dir den entsprechenden Testfall auch zugeschickt, und es ist definitiv schneller mit %.

    Stimmt, das hattest Du ja auf der Spirit Liste geschrieben, hatte ich vergessen... :p

    phlox81 schrieb:

    hkaiser schrieb:

    phlox81 schrieb:

    2.3 spirit::karma

    Dieser Namensraum ist neu. Es handelt sich hier um das Gegenstück zu spirit::qi, welches für die Ausgabe von Daten in einen Ausgabestrom zuständig ist. Karma nutzt hierfür den << operator, analog zu qi, welches den >> operator für die Inputsequenz benutzt.
    Karma ist daher auch vom Aufbau recht ähnlich zu qi, nur das es halt keine Inputregeln sondern Ausgaberegeln definiert. Das letzte Beispiel aus spirit::qi, MiniXML, könnte man ja auch wieder in eine Datei schreiben wollen, um z.B. ein XML Format in ein anderes zu wandeln. Mit Karma ist nun genau dies möglich:

    ...

    Auch hier wird die Ausgabeklasse von einer entsprechenden Basisklasse abgeleitet. Ebenfalls werden ähnliche Regeln wie in der qi Grammatik angewandt. Die Direktive verbatim ist das Gegenstück zu lexeme in qi, verbatim stellt sicher, das keine Leerzeichen im Text sind.

    Richtiger wäre zu sagen: verbatim[] stellt sicher, daß keine zusätzlichen Trennzeichen in die Ausgabe eingefügt werden. Ähnlich wie Spirit.Qi es erlaubt, einen skip-Parser zu verwenden, der bestimmte Zeichen in der Eingabe ignoriert (und man dieses Verhalten mit lexeme[] unterbinden kann), so ermöglicht Spirit.Karma delimit-Generatoren vorzugeben, die zusätzliche Trennsymbole in der Ausgabe einfügen.

    Die Symmetrie zwischen Qi und Karma ist der Schlüssel zum Verständnis der Zusammenhänge. Ist wie beim Welle-Teilchen Dualismus des Lichts. Yin und Yang, das Eine ergänzt das Andere und kann nicht ohne das Andere gesehen werden.

    phlox81 schrieb:

    phlox81 schrieb:

    phlox81 schrieb:

    Die korrekten Werte für die Ausgabe werden von der Hauptregel XML dann auch an die eingehängten Regeln wie z.B. start_tag weitergereicht. Mit Regelname._1 greift man jeweils auf diesen Wert zu.

    Das hat sich geändert. Die richtige Syntax ist hier (genau wie in Qi): regelname._r1 etc.

    k. gut zu wissen. Steht das auch in den Examples?

    Wenn ich mich richtig erinnere, dann funktioniert das Karma mini-XML Beispiel noch nicht, daher wahrscheinlich Deine Verwirrung :p. Ich schau es mir mal an und bringe es in Ordnung.

    phlox81 schrieb:

    hkaiser schrieb:

    Vielleicht solltest Du noch einen Hinweis hinzufügen, der die verwendbaren Compiler beschreibt.

    Alles in allem würde ich gern mit Dir an diesem Artikel zusammenarbeiten. Ich habe hier auch schon einiges Material gesammelt und würde gern Doppelarbeiten vermeiden.

    Ja, da müsste ich halt wissen welche das sind. Werde den Artikel morgen mal überarbeiten, bin noch offen für Vorschläge und Anregungen 🙂

    Man benötigt einen recht Standards-konformen C++ Compiler, um Spirit2 verwenden zu können. Wir haben Spirit2 mit gcc ab V3.3.x und VC ab V7.1 getestet. Der Intel Compiler ab V9.x funktioniert auch, treibt jedoch die Übersetzungszeiten extrem in die Höhe.

    Generell würde ich in einem einführendem Artikel über Spirit2 mehr Informationen über Zusammenhänge und Hintergründe sehen, weniger eine Auflistung von Beispielen, die ohne diese Hintergründe schwer nachvollziehbar sind. Aber das ist nur meine Meinung, andere Leute sehen das vielleicht anders.

    Regards Hartmut

    PS: ich hänge hier mal eine Einleitung für einen Spirit2 Artikel an, so wie ich es gern schreiben würde.

    Der Geist aus der Flasche: Parsen und Ausgabegenerierung in C++ mit Spirit V2

    Viele Programmierer bringen das Parsen von Zeichenketten immer noch mit schwarzer Magie in Zusammenhang, wobei man, um es zu beherrschen, auf solche esotherischen Werkzeuge wie Lex und Yacc zurückgreifen muß. Das führt dazu, daß Parser oft von Hand geschrieben werden, was dem Mythos zusätzlichen Nährboden verschafft.

    Ähnlich verhält es sich mit dem Generieren von (formatierten) Ausgaben. Auch hier wird zumeist auf die altbewährte Handarbeit zurückgegriffen, nur das in diesem Fall kaum Werkzeuge existieren (zumindest nicht in C++), die uns 'normalen' Programmierern helfen könnten, einfach, flexibel und schnell unsere auszugebeneden Zeichenketten zu erzeugen. Da wird oft auf printf und die doch etwas unbequemen C++ Streams zurückgegriffen. Nur selten werden externe Werkzeuge wie Template basierte Generatoren verwendet.

    In beiden Fällen, beim handgeschriebenen Programm für das Parsen oder das Generieren von Ausgaben erhalten wir oft im Ergebniss Code, der nicht nur unübersichtlich und schlecht wartbar ist, sondern gleichzeitig dem falschen Eindruck Vorschub leistet, C++ sei zu komplex für 'normale' Programmierer. Jede kleinste Änderung in den zu Grunde liegenden Datenstrukturen führt dazu, das man große Teile des Codes umschreiben muß.

    Eine Einführung

    Wer sich in der Welt der Parser umsieht, dem fällt (neben BNFC vielleicht) eine Bibliothek ganz besonders auf: Spirit - eine template basierte C++ Bibliothek, die Bestandteil der wohlbekannten Boost Bibliothekssammlung ist. Während die meisten Parser tools auf der einen oder anderen Form von Code Generierung beruhen (oder sogar auf "Code Generierung Generierung" wie im Fall von BNFC), so ist Spirit im Gegensatz dazu eine Meta-Programmierungs-Bibliothek, die sich ausschließlich auf die Möglichkeiten des C++ Compilers selbst stützt. Zur Code Generierung verläßt Spirit nie die Zielsprache: C++ eben. Wem das zu abstrakt klingt, der kann sich das als "coole Dinge mit C++ innerhalb der Sprache selbst machen" vorstellen.

    Die Vorteile sind offensichtlich: man benötigt keine separaten Tools um seinen Parser zu erzeugen, man muß auch keine neue Sprache erlernen, nur um diesen Tools beizubringen, was man von ihnen möchte. Spirit nutzt lediglich die Möglichkeiten, die C++ sowieso bietet. Man kann also Parser schreiben und sich dabei vollständig auf seine Kenntnisse der Sprache verlassen.

    In der Welt der Parsergeneratoren hat sich über lange Zeit bewährt, die Zeichenketten, die durch einen Parser 'verstanden' werden sollen, formal zu beschreiben. Eine der neueren Erkenntnisse auf diesem Gebiet sind die Parser Expression Grammars (PEGs), die im wesentlichen eine Weiterentwicklung von EBNF (Extended Backus-Naur-Form) mit Elementen regulärer Ausdrücke ist. Wir werden weiter unten auf PEGs zurückkommen und diese näher erläutern. Diese Parsergeneratoren sind zumeist als externe Werkzeuge verfügbar, die diese formale Beschreibung einlesen und daraus Code generieren. Dieser Code kann für 2 Dinge verwendet werden. Zum Einen kann man prüfen, ob die Struktur einer Zeichenkette der formalen Beschreibung entspricht, und zum Anderen, um eine 'richtig strukturierte' Zeichenkette in eine passende interne Datenstruktur einzulesen. Spirit greift PEGs auf und verallgemeinert ihren Einsatz nicht nur für das Parsen sondern auch für das Generieren formatierter Ausgaben.

    Wir wollen die Theorie hier verlassen und anschauen, was das für den normalsterblichen Programmierer bedeutet. Angenommen, wir wollen eine Liste von Komma-separierten double Zahlen einlesen (später werden wir diese Zahlen direkt in einem std::vector<double> ablegen). Eine zu simple Aufgabe um einen Parser zu schreiben? Nicht mit Spirit! Hier erstmal der Code, welche die Liste der double Zahlen einlesen kann:

    double_ >> *(',' >> double_)
    

    Das sieht fast wie eine der allseits bekannten Eingabe-(Stream-)Operationen aus, bedeutet aber etwas anderes. Dieser Ausdruck teilt Spirit mit: bitte erkenne für mich in der Eingabezeichenkette eine double Zahl gefolgt von ( >> ) Null oder mehr Vorkommen ( * ) von einem Komma jeweils gefolgt von einer double Zahl - eine Komma-separierte Liste von double's also.

    Dieses kleine Beispiel macht das zentrale Funktionsprinzip von Spirit klar: es benutzt die Eigenschaft von C++, die es erlaubt, fast beliebige Operatoren zu überladen und mit eigener Funktionalität zu versehen. In diesem Fall werden die überladenen Versionen des operator>>() und des operator*() verwendet. Im folgenden wird ein vollständiges (compilierbares) Codebeispiel gegeben, die den eben definierten Parser einsetzt.

    // Dieses Programm versucht das erste Kommandozeilenargument als eine Liste von
    // double's zu interpretieren und meldet, ob es eine ist.
    
    #include <iostream>
    #include <boost/spirit.hpp>
    
    using namespace boost::spirit;
    
    int main(int argc, char* argv[])
    {
        if (argc != 2) {
            std::cerr << "Liste von double's fehlt!" << std::endl;
            return -1;
        }
    
        if (qi::parse (argv[1], double_ >> *(',' >> double_))
            std::cout << "Ja, das ist eine Liste von double's!" << std::endl;
        else
            std::cout << "War nix!" << std::endl;
    
        return 0;
    }
    

    Die Idee eine formale Notation, also eine Grammatik, zu verwenden, um eine erwartete Struktur der zu untersuchenden Eingabe-Zeichenkette zu beschreiben, ist sehr flexibel und leicht verständlich. Offensichtlich läßt sich diese Grammatik aber auch dafür verwenden, die gewünschte Struktur einer entsprechenden Ausgabe-Zeichenkette zu formalisieren. Das funktioniert, weil Parsen zumeist durch den Vergleich der Eingabe mit den formalen Mustern in der Grammatik erfolgt. Wenn man also die selben Muster wieder verwendet um eine passende Ausgabe zu formatieren, wird diese Ausgabe auch den Regeln der Grammatik folgen.

    In Spirit ist dieser Gedanke aufgegriffen und implementiert worden. Wir können das eben an Hand unseres kleinen Beispiels gelernte Wissen anwenden um eine Komma-separierte Liste von double's auszugeben. Die entsprechende Formatierungsregel sieht so aus:

    double_ << *(',' << double_)
    

    Wieder werden C++ Operatoren überladen und mit einer neuen Bedeutung versehen. Die Sequenz mehrerer Ausgabeprimitive wird durch den operator<<() codiert, wobei der operator*() eine identische Bedeutung hat wie im Beispiel oben. Alles in allem teilt diese Regel Spirit mit: bitte erzeuge in der Ausgabe eine Zeichenkette die aus einer double Zahl besteht, gefolgt von Null oder mehr Vorkommen ( * ) eines Kommas, jeweils gefolgt von ebenfalls einer double Zahl.

    Eingabe und Ausgabe sind in Spirit symmetrisch und sich gegenseitig ergänzend implementiert. Viele der Primitive (hier: double_ , beachte den angehängten Unterstrich) sind für beides einsetzbar, viele Konzepte kompatibel. Eingabe und Ausgabe in Spirit sind wie Yin und Yang, die komplementären Seiten ein und der selben Medaille.

    ...



  • Gut, ich werde heute und morgen das dann wieder zu einem Artikel zusammenfügen.
    Deine Einleitung finde ich gut, aber auch etwas langatmig, nicht umsonst habe ich auf den Artikel zu spirit1.x verwiesen,
    denn da steht schon vieles drin, was wir nicht noch mal wiederholen müssen. Zumal ich immer noch denke, das der Artikel
    nur einen kurzen Überblick über spirit2 geben sollte, bevor du dann noch in weiteren Artikel tiefer in die Details eingehst.

    Ich habe jetzt auch einen Unterpunkt Vorwort eingefügt, da würde ich gerne den Text von dir einfügen.
    Allerdings müsstest du ihn noch was kürzen, so finde ich ihn zu lang:

    Der Geist aus der Flasche: Parsen und Ausgabegenerierung in C++ mit Spirit V2

    Viele Programmierer bringen das Parsen von Zeichenketten immer noch mit schwarzer Magie in Zusammenhang, wobei man, um es zu beherrschen, auf solche esotherischen Werkzeuge wie Lex und Yacc zurückgreifen muß. Das führt dazu, daß Parser oft von Hand geschrieben werden, was dem Mythos zusätzlichen Nährboden verschafft.

    Ähnlich verhält es sich mit dem Generieren von (formatierten) Ausgaben. Auch hier wird zumeist auf die altbewährte Handarbeit zurückgegriffen, nur das in diesem Fall kaum Werkzeuge existieren (zumindest nicht in C++), die uns 'normalen' Programmierern helfen könnten, einfach, flexibel und schnell unsere auszugebeneden Zeichenketten zu erzeugen. Da wird oft auf printf und die doch etwas unbequemen C++ Streams zurückgegriffen. Nur selten werden externe Werkzeuge wie Template basierte Generatoren verwendet.

    In beiden Fällen, beim handgeschriebenen Programm für das Parsen oder das Generieren von Ausgaben erhalten wir oft im Ergebniss Code, der nicht nur unübersichtlich und schlecht wartbar ist, sondern gleichzeitig dem falschen Eindruck Vorschub leistet, C++ sei zu komplex für 'normale' Programmierer. Jede kleinste Änderung in den zu Grunde liegenden Datenstrukturen führt dazu, das man große Teile des Codes umschreiben muß.

    Finde ich soweit ok.

    Eine Einführung

    Wer sich in der Welt der Parser umsieht, dem fällt (neben BNFC vielleicht) eine Bibliothek ganz besonders auf: Spirit - eine template basierte C++ Bibliothek, die Bestandteil der wohlbekannten Boost Bibliothekssammlung ist. Während die meisten Parser tools auf der einen oder anderen Form von Code Generierung beruhen (oder sogar auf "Code Generierung Generierung" wie im Fall von BNFC), so ist Spirit im Gegensatz dazu eine Meta-Programmierungs-Bibliothek, die sich ausschließlich auf die Möglichkeiten des C++ Compilers selbst stützt. Zur Code Generierung verläßt Spirit nie die Zielsprache: C++ eben. Wem das zu abstrakt klingt, der kann sich das als "coole Dinge mit C++ innerhalb der Sprache selbst machen" vorstellen.

    Die Vorteile sind offensichtlich: man benötigt keine separaten Tools um seinen Parser zu erzeugen, man muß auch keine neue Sprache erlernen, nur um diesen Tools beizubringen, was man von ihnen möchte. Spirit nutzt lediglich die Möglichkeiten, die C++ sowieso bietet. Man kann also Parser schreiben und sich dabei vollständig auf seine Kenntnisse der Sprache verlassen.

    Hier finde ich trägst du was zu dick auf 😉 Und das Spirit keine neue Sprache in C++ einführt ist imho strittig. Spirit hat in vielen Dingen eben doch seine EIGENE Syntax, welche von der sonst üblichen Syntax abweicht, zweckentfremded Operatoren etc.
    Und Eigenlob stinkt 😉

    In der Welt der Parsergeneratoren hat sich über lange Zeit bewährt, die Zeichenketten, die durch einen Parser 'verstanden' werden sollen, formal zu beschreiben. Eine der neueren Erkenntnisse auf diesem Gebiet sind die Parser Expression Grammars (PEGs), die im wesentlichen eine Weiterentwicklung von EBNF (Extended Backus-Naur-Form) mit Elementen regulärer Ausdrücke ist. Wir werden weiter unten auf PEGs zurückkommen und diese näher erläutern. Diese Parsergeneratoren sind zumeist als externe Werkzeuge verfügbar, die diese formale Beschreibung einlesen und daraus Code generieren. Dieser Code kann für 2 Dinge verwendet werden. Zum Einen kann man prüfen, ob die Struktur einer Zeichenkette der formalen Beschreibung entspricht, und zum Anderen, um eine 'richtig strukturierte' Zeichenkette in eine passende interne Datenstruktur einzulesen. Spirit greift PEGs auf und verallgemeinert ihren Einsatz nicht nur für das Parsen sondern auch für das Generieren formatierter Ausgaben.

    Wir wollen die Theorie hier verlassen und anschauen, was das für den normalsterblichen Programmierer bedeutet. Angenommen, wir wollen eine Liste von Komma-separierten double Zahlen einlesen (später werden wir diese Zahlen direkt in einem std::vector<double> ablegen). Eine zu simple Aufgabe um einen Parser zu schreiben? Nicht mit Spirit! Hier erstmal der Code, welche die Liste der double Zahlen einlesen kann:

    double_ >> *(',' >> double_)
    

    Das sieht fast wie eine der allseits bekannten Eingabe-(Stream-)Operationen aus, bedeutet aber etwas anderes. Dieser Ausdruck teilt Spirit mit: bitte erkenne für mich in der Eingabezeichenkette eine double Zahl gefolgt von ( >> ) Null oder mehr Vorkommen ( * ) von einem Komma jeweils gefolgt von einer double Zahl - eine Komma-separierte Liste von double's also.

    Dieses kleine Beispiel macht das zentrale Funktionsprinzip von Spirit klar: es benutzt die Eigenschaft von C++, die es erlaubt, fast beliebige Operatoren zu überladen und mit eigener Funktionalität zu versehen. In diesem Fall werden die überladenen Versionen des operator>>() und des operator*() verwendet. Im folgenden wird ein vollständiges (compilierbares) Codebeispiel gegeben, die den eben definierten Parser einsetzt.

    // Dieses Programm versucht das erste Kommandozeilenargument als eine Liste von
    // double's zu interpretieren und meldet, ob es eine ist.
    
    #include <iostream>
    #include <boost/spirit.hpp>
    
    using namespace boost::spirit;
    
    int main(int argc, char* argv[])
    {
        if (argc != 2) {
            std::cerr << "Liste von double's fehlt!" << std::endl;
            return -1;
        }
    
        if (qi::parse (argv[1], double_ >> *(',' >> double_))
            std::cout << "Ja, das ist eine Liste von double's!" << std::endl;
        else
            std::cout << "War nix!" << std::endl;
            
        return 0;
    }
    

    Die Idee eine formale Notation, also eine Grammatik, zu verwenden, um eine erwartete Struktur der zu untersuchenden Eingabe-Zeichenkette zu beschreiben, ist sehr flexibel und leicht verständlich. Offensichtlich läßt sich diese Grammatik aber auch dafür verwenden, die gewünschte Struktur einer entsprechenden Ausgabe-Zeichenkette zu formalisieren. Das funktioniert, weil Parsen zumeist durch den Vergleich der Eingabe mit den formalen Mustern in der Grammatik erfolgt. Wenn man also die selben Muster wieder verwendet um eine passende Ausgabe zu formatieren, wird diese Ausgabe auch den Regeln der Grammatik folgen.

    In Spirit ist dieser Gedanke aufgegriffen und implementiert worden. Wir können das eben an Hand unseres kleinen Beispiels gelernte Wissen anwenden um eine Komma-separierte Liste von double's auszugeben. Die entsprechende Formatierungsregel sieht so aus:

    double_ << *(',' << double_)
    

    Wieder werden C++ Operatoren überladen und mit einer neuen Bedeutung versehen. Die Sequenz mehrerer Ausgabeprimitive wird durch den operator<<() codiert, wobei der operator*() eine identische Bedeutung hat wie im Beispiel oben. Alles in allem teilt diese Regel Spirit mit: bitte erzeuge in der Ausgabe eine Zeichenkette die aus einer double Zahl besteht, gefolgt von Null oder mehr Vorkommen ( * ) eines Kommas, jeweils gefolgt von ebenfalls einer double Zahl.

    Eingabe und Ausgabe sind in Spirit symmetrisch und sich gegenseitig ergänzend implementiert. Viele der Primitive (hier: double_ , beachte den angehängten Unterstrich) sind für beides einsetzbar, viele Konzepte kompatibel. Eingabe und Ausgabe in Spirit sind wie Yin und Yang, die komplementären Seiten ein und der selben Medaille.

    ...

    Wie gesagt, ich finde das ganz gelungen, aber evtl. sollte man etwas kürzen. IMHO.

    phlox



  • So, der Artikel ist wieder überarbeitet, warte auf weiteres Feedback bzw. Hartmuts Vorwort 2.0 😉
    Wie immer arbeite ich auf der ersten Artikelversion, also post#1.



  • phlox81 schrieb:

    Gut, ich werde heute und morgen das dann wieder zu einem Artikel zusammenfügen.
    Deine Einleitung finde ich gut, aber auch etwas langatmig, nicht umsonst habe ich auf den Artikel zu spirit1.x verwiesen,
    denn da steht schon vieles drin, was wir nicht noch mal wiederholen müssen.

    Ich habs lieber explizit. But YMMV.

    phlox81 schrieb:

    Zumal ich immer noch denke, das der Artikel
    nur einen kurzen Überblick über spirit2 geben sollte, bevor du dann noch in weiteren Artikel tiefer in die Details eingehst.

    Wenn diese Einleitung zu knapp und unverständlich wird, dann will keiner mehr weiter lesen.

    phlox81 schrieb:

    Ich habe jetzt auch einen Unterpunkt Vorwort eingefügt, da würde ich gerne den Text von dir einfügen.
    Allerdings müsstest du ihn noch was kürzen, so finde ich ihn zu lang:

    Wer sich in der Welt der Parser umsieht, dem fällt (neben BNFC vielleicht) eine Bibliothek ganz besonders auf: Spirit - eine template basierte C++ Bibliothek, die Bestandteil der wohlbekannten Boost Bibliothekssammlung ist. Während die meisten Parser tools auf der einen oder anderen Form von Code Generierung beruhen (oder sogar auf "Code Generierung Generierung" wie im Fall von BNFC), so ist Spirit im Gegensatz dazu eine Meta-Programmierungs-Bibliothek, die sich ausschließlich auf die Möglichkeiten des C++ Compilers selbst stützt. Zur Code Generierung verläßt Spirit nie die Zielsprache: C++ eben. Wem das zu abstrakt klingt, der kann sich das als "coole Dinge mit C++ innerhalb der Sprache selbst machen" vorstellen.

    Die Vorteile sind offensichtlich: man benötigt keine separaten Tools um seinen Parser zu erzeugen, man muß auch keine neue Sprache erlernen, nur um diesen Tools beizubringen, was man von ihnen möchte. Spirit nutzt lediglich die Möglichkeiten, die C++ sowieso bietet. Man kann also Parser schreiben und sich dabei vollständig auf seine Kenntnisse der Sprache verlassen.

    Hier finde ich trägst du was zu dick auf 😉 Und das Spirit keine neue Sprache in C++ einführt ist imho strittig. Spirit hat in vielen Dingen eben doch seine EIGENE Syntax, welche von der sonst üblichen Syntax abweicht, zweckentfremded Operatoren etc.
    Und Eigenlob stinkt 😉

    Wenn man sich nicht selbst lobt, dann lobt einen keiner :p

    phlox81 schrieb:

    Wie gesagt, ich finde das ganz gelungen, aber evtl. sollte man etwas kürzen. IMHO.

    Es ist Dein Artikel, also Deine Entscheidung...

    Regards Hartmut



  • Ich muss phlox81 da recht geben. Sätze wie

    Dieses kleine Beispiel macht das zentrale Funktionsprinzip von Spirit klar: es benutzt die Eigenschaft von C++, die es erlaubt, fast beliebige Operatoren zu überladen und mit eigener Funktionalität zu versehen. In diesem Fall werden die überladenen Versionen des operator>>() und des operator*() verwendet.

    sind zwar korrekt jedoch meiner Meinung nach überflüssig. Sie sagen nichts was das Code Beispiel nicht bereits deutlich gemacht hat. Dies bläht die Einleitung nur unnötig auf und vor allem für eine Einleitung ist dies tödlich.

    Ein anderes Beispiel:

    Wer sich in der Welt der Parser umsieht, dem fällt (neben BNFC vielleicht) eine Bibliothek ganz besonders auf: Spirit - eine template basierte C++ Bibliothek, die Bestandteil der wohlbekannten Boost Bibliothekssammlung ist.

    Alle Aussagen sind völlig korrekt, jedoch wer sich in Spirit einliest, der wird dies schon wissen.

    Wenn diese Einleitung zu knapp und unverständlich wird, dann will keiner mehr weiter lesen.

    Knapp und unverständlich sind aber an sich orthogonal. Man kann etwas sehr wohl knapp und verständlich ausdrücken.



  • Ben04 schrieb:

    sind zwar korrekt jedoch meiner Meinung nach überflüssig. Sie sagen nichts was das Code Beispiel nicht bereits deutlich gemacht hat. Dies bläht die Einleitung nur unnötig auf und vor allem für eine Einleitung ist dies tödlich.

    Sie erinnern aber den Leser an einige Sachen, die er schon kennt. Das hilft am Ball zu bleiben und das Gelesene direkt einzuordnen.

    Wer sich in der Welt der Parser umsieht, dem fällt (neben BNFC vielleicht) eine Bibliothek ganz besonders auf: Spirit - eine template basierte C++ Bibliothek, die Bestandteil der wohlbekannten Boost Bibliothekssammlung ist.

    Alle Aussagen sind völlig korrekt, jedoch wer sich in Spirit einliest, der wird dies schon wissen.

    Und derjenige, der den Artikel einfach mal anliest um zu sehen was das ist? Du solltest nicht davon ausgehen, dass nur Leute den Artikel lesen, die sich schon damit auskennen oder sich sowieso schon mit dem Thema befasst haben.

    Wenn diese Einleitung zu knapp und unverständlich wird, dann will keiner mehr weiter lesen.

    Knapp und unverständlich sind aber an sich orthogonal. Man kann etwas sehr wohl knapp und verständlich ausdrücken.

    Eine zu knappe Ausdrucksweise wird automatisch unverständlich (soweit zur Orthogonalität). Ein gelegentliches wiederholen von bekannten Infos ist nicht schlecht. Für einige ist es trotzdem neu, die sagen sich dann "aha, so funktioniert das!" während andere, die's schon kennen wissend mit dem Kopf nicken. Das Einordnen in Zusammenhänge und die Verknüpfung zu bekannten/verwandten Themen ist ein wichtiger Teil jeder Arbeit.



  • Denk bitte daran, dass der Artikel heute auf [R] gehen sollte. 🙂



  • So, letzte Änderungen sind drin.
    Vorwort von Hartmut Kaiser hab ich jetzt komplett übernommen, kürze nicht gern in fremden Texten.
    Bei qi habe ich noch das xml Beispiel rausgenommen, da das sonst zu viel wird.

    Damit ist der Artikel jetzt auf R 🙂



  • phlox81 schrieb:

    2.3 spirit::karma

    Dieser Namensraum ist neu. Es handelt sich hier um das Gegenstück zu spirit::qi, welches für die Ausgabe von Daten in einen Ausgabestrom zuständig ist. Karma nutzt hierfür den << operator, analog zu qi, welches den >> operator für die Inputsequenz benutzt.
    Karma ist daher auch vom Aufbau recht ähnlich zu qi, nur das es halt keine Inputregeln sondern Ausgaberegeln definiert. Das letzte Beispiel aus spirit::qi, MiniXML, könnte man ja auch wieder in eine Datei schreiben wollen, um z.B. ein XML Format in ein anderes zu wandeln. Mit Karma ist nun genau dies möglich:

    template <typename OutputIterator>
    struct mini_xml_generator
      : boost::spirit::karma::grammar_def<OutputIterator, void(mini_xml), space_type>
    {
     typedef karma::grammar_def<OutputIterator, void(mini_xml), space_type> base_type;
     boost::mpl::print<typename base_type::start_type::param_types> x;
    

    Die vorherige Zeile ist ein debug-Überest und gehört nicht hier hinein.

    phlox81 schrieb:

    mini_xml_generator()
        {
                 text = verbatim[lit(text._1)];
                 node = (xml | text)[_1 = node._1];
    
                 start_tag =
                         '<'
                     <<  verbatim[lit(start_tag._1)]
                     <<  '>'
                 ;
    
                 end_tag =
                         "</"
                     <<  verbatim[lit(end_tag._1)]
                     <<  '>'
                 ;
    
             xml =
                     start_tag(at_c<0>(xml._1))
                     <<  (*node)[ref(at_c<1>(xml._1))]
                 <<  end_tag(at_c<0>(xml._1))
            ;
        }
    
        karma::rule<OutputIterator, void(mini_xml), space_type> xml;
        karma::rule<OutputIterator, void(mini_xml_node), space_type> node;
        karma::rule<OutputIterator, void(std::string), space_type> text;
        karma::rule<OutputIterator, void(std::string), space_type> start_tag;
        karma::rule<OutputIterator, void(std::string), space_type> end_tag;
    };
    

    Die richtige Syntax ist hier (genau wie in Qi): regelname._r1 an Stelle von regelname._1 etc.

    Regards Hartmut



  • Stimmt. Hab ich jetzt noch beides korrigiert.
    Danke für den Input 🙂



  • Kannst Du den Satz: 'Wir werden weiter unten auf PEGs zurückkommen.' in der Einleitung rausnehmen? Dieser ist noch drin weil ich diese Einleitung ursprünglich selbst verwenden wollte...

    Ein kleiner Fehler noch:

    phlox81 schrieb:

    xml =
                     start_tag(at_c<0>(xml._1))
                     <<  (*node)[ref(at_c<1>(xml._1))]
                 <<  end_tag(at_c<0>(xml._1))
            ;
    

    Du hast hier immer noch regelname._1 drin.

    Regards Hartmut



  • Kann ich machen.

    Das zweite erschien mir vom Kontext her logischer, da r1 ja übergeben ist, wenn es trotzdem falsch ist, kann ich es aber korrigieren.



  • 1 Vorbemerkungen

    Dieser Artikel beschäftigt sich mit boost::spirit 2.0, welches der Nachfolger von boost::spirit1.x ist.
    Für Einsteiger empfiehlt sich deshalb auch die Lektüre von Boost::Spirit - Eine Einführung, da ich nicht auf alle Details der Syntax eingehen werde.

    Auch gilt für die meisten Codebeispiele, dass die Namespaces eingebunden werden:

    using namespace boost::spirit;
    using namespace boost::spirit::qi;
    using namespace boost::spirit::ascii;
    using namespace boost::spirit::arg_names;
    using boost::phoenix::ref;
    

    1.1 Vorwort

    Hartmut Kaiser, einer der Spiritentwickler, war so freundlich, das Vorwort zu diesem Artikel zu schreiben:

    Der Geist aus der Flasche: Parsen und Ausgabegenerierung in C++ mit Spirit V2

    Viele Programmierer bringen das Parsen von Zeichenketten immer noch mit schwarzer Magie in Zusammenhang, wobei man, um es zu beherrschen, auf solche esotherischen Werkzeuge wie Lex und Yacc zurückgreifen muss. Das führt dazu, dass Parser oft von Hand geschrieben werden, was dem Mythos zusätzlichen Nährboden verschafft.

    Ähnlich verhält es sich mit dem Generieren von (formatierten) Ausgaben. Auch hier wird zumeist auf die altbewährte Handarbeit zurückgegriffen, nur dass in diesem Fall kaum Werkzeuge existieren (zumindest nicht in C++), die uns 'normalen' Programmierern helfen könnten, einfach, flexibel und schnell unsere auszugebeneden Zeichenketten zu erzeugen. Da wird oft auf printf und die doch etwas unbequemen C++-Streams zurückgegriffen. Nur selten werden externe Werkzeuge wie templatebasierte Generatoren verwendet.

    In beiden Fällen, beim handgeschriebenen Programm für das Parsen und für das Generieren von Ausgaben erhalten wir oft im Ergebnis Code, der nicht nur unübersichtlich und schlecht wartbar ist, sondern gleichzeitig dem falschen Eindruck Vorschub leistet, C++ sei zu komplex für 'normale' Programmierer. Jede kleinste Änderung in den zu Grunde liegenden Datenstrukturen führt dazu, dass man große Teile des Codes umschreiben muss.

    Eine Einführung

    Wer sich in der Welt der Parser umsieht, dem fällt (neben BNFC vielleicht) eine Bibliothek ganz besonders auf: Spirit - eine templatebasierte C++-Bibliothek, die Bestandteil der wohlbekannten Boost-Bibliothekssammlung ist. Während die meisten Parsertools auf der einen oder anderen Form von Codegenerierung beruhen (oder sogar auf "Codegenerierung-Generierung" wie im Fall von BNFC), so ist Spirit im Gegensatz dazu eine Meta-Programmierungs-Bibliothek, die sich ausschließlich auf die Möglichkeiten des C++-Compilers selbst stützt. Zur Codegenerierung verlässt Spirit nie die Zielsprache: C++ eben. Wem das zu abstrakt klingt, der kann sich das als "coole Dinge mit C++ innerhalb der Sprache selbst machen" vorstellen.

    Die Vorteile sind offensichtlich: man benötigt keine separaten Tools um seinen Parser zu erzeugen, man muss auch keine neue Sprache erlernen, nur um diesen Tools beizubringen, was man von ihnen möchte. Spirit nutzt lediglich die Möglichkeiten, die C++ sowieso bietet. Man kann also Parser schreiben und sich dabei vollständig auf seine Kenntnisse der Sprache verlassen.

    In der Welt der Parsergeneratoren hat sich über lange Zeit bewährt, die Zeichenketten, die durch einen Parser 'verstanden' werden sollen, formal zu beschreiben. Eine der neueren Erkenntnisse auf diesem Gebiet sind die Parser Expression Grammars (PEGs), die im wesentlichen eine Weiterentwicklung von EBNF (Extended Backus-Naur-Form) mit Elementen regulärer Ausdrücke sind. Wir werden weiter unten auf PEGs zurückkommen und diese näher erläutern. Diese Parsergeneratoren sind zumeist als externe Werkzeuge verfügbar, die diese formale Beschreibung einlesen und daraus Code generieren. Dieser Code kann für 2 Dinge verwendet werden. Zum Einen kann man prüfen, ob die Struktur einer Zeichenkette der formalen Beschreibung entspricht, und zum Anderen, um eine 'richtig strukturierte' Zeichenkette in eine passende interne Datenstruktur einzulesen. Spirit greift PEGs auf und verallgemeinert ihren Einsatz nicht nur für das Parsen sondern auch für das Generieren formatierter Ausgaben.

    Wir wollen die Theorie hier verlassen und anschauen, was das für den normalsterblichen Programmierer bedeutet. Angenommen, wir wollen eine Liste von Komma-separierten double -Zahlen einlesen (später werden wir diese Zahlen direkt in einem std::vector<double> ablegen). Eine zu simple Aufgabe um einen Parser zu schreiben? Nicht mit Spirit! Hier erstmal der Code, welche die Liste der double Zahlen einlesen kann:

    double_ >> *(',' >> double_)
    

    Das sieht fast wie eine der allseits bekannten Eingabe-(Stream-)Operationen aus, bedeutet aber etwas anderes. Dieser Ausdruck teilt Spirit mit: bitte erkenne für mich in der Eingabezeichenkette eine double Zahl gefolgt von ( >> ) null oder mehr Vorkommen ( * ) von einem Komma jeweils gefolgt von einer double -Zahl - eine Komma-separierte Liste von doubles also.

    Dieses kleine Beispiel macht das zentrale Funktionsprinzip von Spirit klar: es benutzt die Eigenschaft von C++, die es erlaubt, fast beliebige Operatoren zu überladen und mit eigener Funktionalität zu versehen. In diesem Fall werden die überladenen Versionen des operator>>() und des operator*() verwendet. Im folgenden wird ein vollständiges (kompilierbares) Codebeispiel gegeben, die den eben definierten Parser einsetzt.

    // Dieses Programm versucht das erste Kommandozeilenargument als eine Liste von
    // doubles zu interpretieren und meldet, ob es eine ist.
    
    #include <iostream>
    #include <boost/spirit.hpp>
    
    using namespace boost::spirit;
    
    int main(int argc, char* argv[])
    {
        if (argc != 2) {
            std::cerr << "Liste von doubles fehlt!" << std::endl;
            return -1;
        }
    
        if (qi::parse (argv[1], double_ >> *(',' >> double_))
            std::cout << "Ja, das ist eine Liste von doubles!" << std::endl;
        else
            std::cout << "War nix!" << std::endl;
    
        return 0;
    }
    

    Die Idee, eine formale Notation, also eine Grammatik, zu verwenden, um eine erwartete Struktur der zu untersuchenden Eingabe-Zeichenkette zu beschreiben, ist sehr flexibel und leicht verständlich. Offensichtlich läßt sich diese Grammatik aber auch dafür verwenden, die gewünschte Struktur einer entsprechenden Ausgabe-Zeichenkette zu formalisieren. Das funktioniert, weil Parsen zumeist durch den Vergleich der Eingabe mit den formalen Mustern in der Grammatik erfolgt. Wenn man also die selben Muster wieder verwendet um eine passende Ausgabe zu formatieren, wird diese Ausgabe auch den Regeln der Grammatik folgen.

    In Spirit ist dieser Gedanke aufgegriffen und implementiert worden. Wir können das eben anhand unseres kleinen Beispiels gelernte Wissen anwenden, um eine Komma-separierte Liste von doubles auszugeben. Die entsprechende Formatierungsregel sieht so aus:

    double_ << *(',' << double_)
    

    Wieder werden C++-Operatoren überladen und mit einer neuen Bedeutung versehen. Die Sequenz mehrerer Ausgabeprimitive wird durch den operator<<() codiert, wobei der operator*() eine identische Bedeutung hat wie im Beispiel oben. Alles in allem teilt diese Regel Spirit mit: bitte erzeuge in der Ausgabe eine Zeichenkette die aus einer double -Zahl besteht, gefolgt von null oder mehr Vorkommen ( * ) eines Kommas, jeweils gefolgt von ebenfalls einer double -Zahl.

    Eingabe und Ausgabe sind in Spirit symmetrisch und sich gegenseitig ergänzend implementiert. Viele der Primitive (hier: double_ , man beachte den angehängten Unterstrich) sind für beides einsetzbar, viele Konzepte kompatibel. Eingabe und Ausgabe in Spirit sind wie Yin und Yang, die komplementären Seiten ein und der selben Medaille.

    1.2 Was ist neu? Was ist anders?

    Als allererstes sollte man vielleicht sagen, dass Spirit2 nicht unbedingt kompatibel zu seinem Vorgänger ist, da es die Erfahrungen und das Feedback der Spiritcommunitiy aufgreift, und von Grund auf neu entwickelt wurde. Zwar blieb bewährtes, wie z.B. die bekannten Regeldefinitionen, aber vieles hat sich verändert bzw. wurde erweitert oder neu hinzugefügt.

    Die wohl größte Neuerung ist, dass Spirit2 nun über drei neue Toplevel-Namespaces verfügt:

    • spirit::lex - ein Modul für die Definition von Lexern
    • spirit::qi - das Modul für die Definition von Regeln zum Parsen von Input
    • spirit::karma - Ein Modul zum Formatieren der Ausgabe

    Auf die Details zu diesen Modulen gehe ich später noch genauer ein.

    2 Drei Beispiele für lex, qi & karma

    Um einen Überblick über die neuen Bereiche in Spirit zu bekommen, ist es am besten, sich einige Beispiele selbst anzuschauen.
    Leider gibt es noch keine Onlinedokumentation oder ähnliches, aus dem man mehr Informationen beziehen kann.
    Aber es gibt die Möglichkeit, im Example-Verzeichnis zu lesen.

    2.1 spirit::lex

    Lex ist der neue Bereich für Lexer in Spirit. Es gibt die Möglichkeit, auch andere Lexer in Spirit zu verwenden, als den Standardlexer von Spirit. Lex dient dazu, den Zeichenstrom in Tokens aufzuteilen, sodass die eigentlichen Regeln des Parsers auf den Token des Lexers und nicht mit dem Eingabestrom arbeitet. Dies ist allerdings optional, es besteht in Spirit2.0 kein Zwang, einen Lexer zu benutzen. Allerdings ist dieser Bereich gerade im Umbruch, da einige Anregungen von der BoostCon im Frühjahr umgesetzt werden. Daher belasse ich es auch bei einem kurzen Lexerbeispiel, später wird es wahrscheinlich einen eigenständigen Artikel über die Möglichkeiten eines Lexers in Spirit2 geben.

    Ein kleines Beispiel für die Definition eines Lexers in Spirit2.0:

    template <typename Lexer>
    struct example1_tokens : lexer_def<Lexer> // lexer_def<Lexer> ist die Basisklasse für Lexer in Spirit2.0
    {
        template <typename Self>
        void def (Self& self)
        {
            // define tokens and associate them with the lexer
            identifier = "[a-zA-Z_][a-zA-Z0-9_]*";
            self = token_def<>(',') | '{' | '}' | identifier;
    
            // any token definition to be used as the skip parser during parsing 
            // has to be associated with a separate lexer state (here 'WS') 
            white_space = "[ \\t\\n]+";
            self("WS") = white_space;
        }
        token_def<> identifier, white_space;
    };
    

    Von oben nach unten:
    Als erstes wird von lexer_def<Lexer> eine Lexerklasse abgeleitet. "lexer_def<Lexer>" stellt die Basisklasse für Lexer in Spirit2.0 dar. Danach wird eine Templatemethode für die Definition der Lexerregeln erstellt. Self ist hierbei die Startregel des Lexers. Vorher wird die Tokenregel für identifier definiert, welches wie man hier sieht auch als Regular Expression geht. Die Definition von Self ist dann wieder (fast) Spirittypisch: eine Verkettung von verschiedenen Regeln.
    Die Definition des Skipparsers erfolgt darunter, wieder mit einer RegEx. Damit diese Regel als Skipparser aktiv wird, muss sie mit dem Lexerstatus "WS" verbunden werden. Skipparser bedeutet, dass diese Zeichen im Zeichenstrom bei der Tokengenerierung ignoriert/übersprungen werden.

    2.2 spirit::qi

    Mit spirit::qi und spirit::karma gibt es nun auch einen eigenständigen Bereich für die Textverarbeitung (parsen), wobei sich spirit::qi um den Input in der Textverarbeitung kümmert. Es gibt einige Veränderungen in diesem Bereich, der quasi auf die Erfahrungen von Spirit1.x aufsetzt. So haben sich die Grammatiken verändert und einige Operatoren sind neu, sowie eine neue Möglichkeit für die Fehlerbehandlung.

    2.2.1 Regeldefinitionen

    So gibt es nun mehrere Möglichkeiten, die Regel zum Einlesen einer CSV-Liste zu definieren:

    double_ >> *(',' >> double_)
    

    Dies ist schon einmal die "Standardregel", mit der man eine solche Aufgabe lösen könnte. Es gibt für Datentypen vordefinierte Regeln, hier double_. (Es gibt auch int_,char_, etc.) Jedoch speichert diese Regel noch keine Daten. Wenn wir die Werte in einen vector<double> ablegen wollen, sieht dies so aus:

    using boost::spirit::fusion::push_back;
    double_[push_back(v,_1)] >> *( ',' >> double_[push_back(v,_1)])
    

    Hier ist push_back eine Funktion aus Fusion, welche das Ergebnis der double_-Regel in den Vector v einfügt. Die Klammern [] stellen wie in Spirit1.x den Block für die semantic Action. _1 ist ein Platzhalter für den Rückgabewert der angewendeten Regel.
    Wie in Spirit1.x gibt es auch in Spirit2 einen Listoperator, der solche Konstrukte verkürzt, und performanter ist als obige Regel:

    double_[push_back(v,_1)] % ','
    

    Diese Regel macht dasselbe wie die Regel oben, jedoch in einer verkürzten Schreibweise.
    Spirit2 erlaubt es, für bestimmte Regeln Defaulttypen anzugeben, bzw. definiert für bestimmte Regeln schon Defaulttypen. Bei % ist dies ein std::vector + der Defaulttyp der vorranstehenden Regel (hier also ein std::vector<double>). Somit lässt sich nun auch ohne Fusion der Vector füllen, indem man beim Aufruf der Parsingfunktion entsprechend den Vector v übergibt:

    bool r = phrase_parse(first, last,( double_ % ',' ),v, space);
    

    2.2.2 Grammatiken

    Wie schon in Spirit1.x gibt es auch in Spirit2 die Möglichkeit, Regeldefinitionen in Grammatiken zusammenzufassen. Die Syntax dieser Grammatiken hat sich jedoch geändert. Während in Spirit1.x noch verschachteltes Definition Struct existierte, so erfolgt jetzt die Definition der Regeln in dem eigentlichen Grammatikkonstruktor.

    Ein Beispiel für Spirit2-Grammatikklassen:

    template <typename Iterator>
    struct roman : grammar_def<Iterator, unsigned()>
    {
        roman()
        {
            start
                =   +char_('M') [_val += 1000]
                ||  hundreds    [_val += _1]
                ||  tens        [_val += _1]
                ||  ones        [_val += _1];
    
            //  Note the use of the || operator. The expression
            //  a || b reads match a or b and in sequence. Try
            //  defining the roman numerals grammar in YACC or
            //  PCCTS. Spirit rules! :-)
        }
    
        rule<Iterator, unsigned()> start;
    };
    

    Die Grammatik wird als Template realisiert, damit sie auf unterschiedlichen Iteratoren anwendbar ist. Sie wird von einer entsprechenden Basisklasse (grammar_def<>) abgeleitet. Die beiden Parameter sind zum einen die Weiterleitung des Iteratortypen, und zum anderen die Definition des Defaulttypen dieser Grammatik. Es gibt noch zwei weitere, hier nicht sichtbare Parameter. Der 3. Parameter dient zur Definition sogenannter locals, also lokal gültiger Variablen innerhalb der Grammatik (locals<int,double,char>, angesprochen über _a,_b bzw. _c.). Der 4. Parameter dient zur Definition eines gültigen Skipparsers.
    Im Konstruktor wird nun die Startregel definiert, wobei start die "default"-Startregel ist. Dies lässt sich ändern, in dem man die Startregel als 2. Parameter im Grammarkonstruktoraufruf einsetzt. Der ||-Operator stellt in Spirit ein sequenzielles Oder dar, sodass hier jede Regel ausgewertet wird, wenn sie erfolgreich angewendet wird, oder bei keinem Match die darauffolgende Regel. _val stellt den Defaultwert der Grammatik dar, quasi ihren Rückgabewert. Dieser wird hier einfach mit dem Ergebnis der Regel addiert. Die Grammatik soll so eine römische in eine arabische Zahl konvertieren. Am Ende wird dann noch die Startregel definiert. Richtig, es fehlt die Definition der anderen Regeln. Wie man die Regeln eines Lexers in Spirit auch in einer Grammatik verwenden kann, so ist es möglich auch andere Elemente als Regeln zu verwenden. Hier sind dies Symboltabellen, in dem Symbole aus dem Eingabestrom mit festen Werten verbunden werden.

    Und so sieht dann eine Symboltabelle in Spirit2 aus:

    struct ones_ : symbols<char, unsigned>
    {
        ones_()
        {
            add
                ("I"    , 1)
                ("II"   , 2)
                ("III"  , 3)
                ("IV"   , 4)
                ("V"    , 5)
                ("VI"   , 6)
                ("VII"  , 7)
                ("VIII" , 8)
                ("IX"   , 9)
            ;
        }
    
    } ones;
    

    Auch hier wird ein Struct definiert, aber diesmal von symbols abgeleitet, mit den Typen char und unsingend. Welche die "Spalten" in der Tabelle darstellen, das heißt, es wird ein oder mehrere chars aus dem Input mit einem unsigned-Wert verknüpft. Im Konstruktor wird nun mit der Funktion add die Tabelle mit Symbolen gefüllt, welche dann später bei der Regelanwendung mit dem Eingabestrom verglichen werden, wobei das Symbol bevorzugt wird, was den meisten Input abdeckt (siehe "I,II,III").

    2.3 spirit::karma

    Dieser Namensraum ist neu. Es handelt sich hier um das Gegenstück zu spirit::qi und ist für die Ausgabe von Daten in einen Ausgabestrom zuständig. Karma nutzt hierfür den <<-Operator, analog zu qi, welches den >>-Operator für die Inputsequenz benutzt.
    Karma ist daher auch vom Aufbau recht ähnlich zu qi, nur dass es keine Inputregeln, sondern Ausgaberegeln definiert. So könnte man mit spirit::karma z.B. XML ausgeben, welches man vorher mit spirit::qi eingelesen hat. Somit hätte man die Möglichkeit einen XML-Filter/Transformator zu schreiben:

    template <typename OutputIterator>
    struct mini_xml_generator
      : boost::spirit::karma::grammar_def<OutputIterator, void(mini_xml), space_type>
    {
        mini_xml_generator()
        {
                 text = verbatim[lit(text._r1)];
                 node = (xml | text)[_1 = node._r1];
    
                 start_tag =
                         '<'
                     <<  verbatim[lit(start_tag._r1)]
                     <<  '>'
                 ;
    
                 end_tag =
                         "</"
                     <<  verbatim[lit(end_tag._r1)]
                     <<  '>'
                 ;
    
             xml =
                     start_tag(at_c<0>(xml._1))
                     <<  (*node)[ref(at_c<1>(xml._1))]
                 <<  end_tag(at_c<0>(xml._1))
            ;
        }
    
        karma::rule<OutputIterator, void(mini_xml), space_type> xml;
        karma::rule<OutputIterator, void(mini_xml_node), space_type> node;
        karma::rule<OutputIterator, void(std::string), space_type> text;
        karma::rule<OutputIterator, void(std::string), space_type> start_tag;
        karma::rule<OutputIterator, void(std::string), space_type> end_tag;
    };
    

    Auch hier wird die Ausgabeklasse von einer entsprechenden Basisklasse abgeleitet. Ebenfalls werden ähnliche Regeln wie in der qi-Grammatik angewandt. Die Direktive verbatim ist das Gegenstück zu lexeme in qi, verbatim stellt sicher, dass keine zusätzlichen Trennzeichen in den Text eingefügt werden. Die korrekten Werte für die Ausgabe werden von der Hauptregel xml dann auch an die eingehängten Regeln wie z.B. start_tag weitergereicht. Mit Regelname._r1 greift man jeweils auf diesen Wert zu.

    3 Infos zu Spirit2

    Spirit2 wurde in diesem Frühjahr auf der Boostkonferenz in Aspen, Colorado vorgestellt. Es ist soweit lauffähig und einsatzbereit. Allerdings gibt es noch keine offizielle Dokumentation, wie wir sie z.B. von Spirit1.x kennen. Es existiert ein SVN-Verzeichnis, welches auch Examples und einige Präsentationen enthält. Daran habe ich mich auch für meinen Vortrag auf dem Treffen und diesen Artikel orientiert. Wer nicht die Möglichkeit hat, Spirit2 aus dem SVN-Verzeichnis zu beziehen, kann sich hier auch den Snapshot von der Boostkonferenz herunterladen.
    Spirit2 hat einige Abhängigkeiten zum aktuellen cvs::HEAD von Boost, somit benötigt man einen aktuellen Checkout von Boost um Spirit2 zu kompilieren. Dabei geht man so vor, dass man in einem Verzeichnis den aktuellen Checkout und in einem anderen Spirit liegen hat, bei den Pfadangaben des Compilers/der IDE muss man nun dafür sorgen, dass der Compiler zuerst das Spirit2-Verzeichnis findet, und dann erst das boost-Verzeichnis.
    Wie sein Vorgänger benötigt Spirit2 einen recht standardkonformen Compiler, wie zum Beispiel gcc3.3.x, VC ab VC7.1 oder Intel9.x, wobei Intel recht hohe Übersetzungszeiten für Spiritprogramme hat.



  • Gut, danke fürs korrigieren, Änderungen sind soweit eingebaut 🙂

    Häng den Artikel jetzt hier noch mal rein, der erste Post ist aber auch auf dem aktuellen Stand.


Anmelden zum Antworten