From 7998c8d7245778201f67b3feef304c07fbfc8a58 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Fri, 17 Jan 2025 18:40:44 +0100 Subject: [PATCH] Library: need support for specification parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unfortunately, there are some common syntactic structures, which can not easily be dissected by regular expressions alone, since they entail nested subexpressions. While it is possible to get beyond those fundamental limitations with some trickery, doing so remains precisely that, ''trickery.'' After fighting some inner conflicts, since ''I do know how to write a parser'' — in the end I have brought myself to just do it. And indeed, as you'd might expect, I have looked into existing library solutions, and I would not like to have any one of them as part of the project. * I do not want a ''parser engine'' or ''parser generator'' * I want the directness of recursive-descent, but combined with Regular Expressions as terminal * I want to see the structure of the used grammar at the definition site of the custom parser function * I want deep integration of ''model bindings'' into the parse process, i.e. binding-λ * I do not want to write model-dissecting or pattern-matching code after the parse * I do not want to expose ''Monads'' as an interface, since they tend to spread unhealthy structure to surrounding code * I do not want to leak technicalities of the parse mechanics into the using code * I do not want to impose hard to remember specific conventions onto the user Thus I've set the following aims: * The usage should require only a single header include (ideally header-only) * The entrance point should be a small number of DSL-starter functions * The parser shall be implemented by recursive-descent, using the parser-combinator technique * But I want that wrapped into a DSL, to be able to control what is (not) provided or exposed. * I want a stateful, applicative logic, since parsing, by its very nature, is stateful! * I want complete compile-time typing, visible to the optimiser, without a virtual »Parser« interface And last but not least, ''I do not want to create a ticket, since I do not know if those goals can be achieved...'' --- doc/technical/howto/crackNuts.txt | 2 +- src/lib/meta/typelist.hpp | 21 +- src/lib/meta/typeseq-util.hpp | 40 + src/lib/meta/variadic-helper.hpp | 69 +- src/lib/parse.hpp | 388 +++++++++ src/lib/regex.hpp | 20 +- src/steam/control/command-def.hpp | 1 - src/steam/engine/proc-node.cpp | 3 +- tests/15library.tests | 5 + tests/library/parse-test.cpp | 192 +++++ wiki/thinkPad.ichthyo.mm | 1228 ++++++++++++++++++++++++++++- 11 files changed, 1883 insertions(+), 86 deletions(-) create mode 100644 src/lib/parse.hpp create mode 100644 tests/library/parse-test.cpp diff --git a/doc/technical/howto/crackNuts.txt b/doc/technical/howto/crackNuts.txt index a8dfc92cf..b3c455115 100644 --- a/doc/technical/howto/crackNuts.txt +++ b/doc/technical/howto/crackNuts.txt @@ -183,7 +183,7 @@ pick and manipulate individually:: - but sometimes it is easier to use the tried and true technique of the Loki-Typelists, which can be programmed recursively, similar to LISP. The »bridge« is to unpack the variadic argument pack into the `lib::meta::Types` ([yellow-background]#⚠ still broken in 2024# - see https://issues.lumiera.org/ticket/987[#987], use `lib::meta::TySeq` from 'variadic-helper.hpp' as workaround...) + see https://issues.lumiera.org/ticket/987[#987], use `lib::meta::TySeq` from 'meta/typelist.hpp' as workaround...) + apply functor to each tuple element:: A common trick is to use `std::apply` in combination with a _fold-expression_ diff --git a/src/lib/meta/typelist.hpp b/src/lib/meta/typelist.hpp index 83c605225..17326d271 100644 --- a/src/lib/meta/typelist.hpp +++ b/src/lib/meta/typelist.hpp @@ -133,6 +133,25 @@ namespace meta { }; - + //////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #987 temporary WORKAROUND -- to be obsoleted + /** + * temporary workaround: + * alternative definition of "type sequence", + * already using variadic template parameters. + * @remarks the problem with our existing type sequence type + * is that it fills the end of each sequence with NullType, + * which was the only way to get a flexible type sequence + * prior to C++11. Unfortunately these trailing NullType + * entries do not play well with other variadic defs. + * @deprecated when we switch our primary type sequence type + * to variadic parameters, this type will be obsoleted. ////////////////////////////////////TICKET #987 : make lib::meta::Types variadic + */ + template + struct TySeq + { + using Seq = TySeq; + using List = typename Types::List; + }; + //////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #987 temporary WORKAROUND(End) -- to be obsoleted }} // namespace lib::meta #endif diff --git a/src/lib/meta/typeseq-util.hpp b/src/lib/meta/typeseq-util.hpp index 0bc97b4ce..d3e30d14e 100644 --- a/src/lib/meta/typeseq-util.hpp +++ b/src/lib/meta/typeseq-util.hpp @@ -143,6 +143,46 @@ namespace meta { + //////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #987 temporary WORKAROUND -- to be obsoleted + /** + * temporary workaround: additional specialisation for the template + * `Prepend` to work also with the (alternative) variadic TySeq. + * @see typeseq-util.hpp + */ + template + struct Prepend> + { + using Seq = TySeq; + using List = typename Types::List; + }; + //////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #987 temporary WORKAROUND -- to be obsoleted + + + /** + * temporary workaround: strip trailing NullType entries from a + * type sequence, to make it compatible with new-style variadic + * template definitions. + * @note the result type is a TySec, to keep it apart from our + * legacy (non-variadic) lib::meta::Types + * @deprecated necessary for the transition to variadic sequences ////////////////////////////////////TICKET #987 : make lib::meta::Types variadic + */ + template + struct StripNullType; + + template + struct StripNullType> + { + using TailSeq = typename StripNullType>::Seq; + + using Seq = typename Prepend::Seq; + }; + + template + struct StripNullType> + { + using Seq = TySeq<>; // NOTE: this causes the result to be a TySeq + }; + //////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #987 temporary WORKAROUND(End) -- to be obsoleted diff --git a/src/lib/meta/variadic-helper.hpp b/src/lib/meta/variadic-helper.hpp index 66114730c..681138bce 100644 --- a/src/lib/meta/variadic-helper.hpp +++ b/src/lib/meta/variadic-helper.hpp @@ -50,74 +50,7 @@ namespace lib { namespace meta { - - //////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #987 temporary WORKAROUND -- to be obsoleted - /** - * temporary workaround: - * alternative definition of "type sequence", - * already using variadic template parameters. - * @remarks the problem with our existing type sequence type - * is that it fills the end of each sequence with NullType, - * which was the only way to get a flexible type sequence - * prior to C++11. Unfortunately these trailing NullType - * entries do not play well with other variadic defs. - * @deprecated when we switch our primary type sequence type - * to variadic parameters, this type will be obsoleted. ////////////////////////////////////TICKET #987 : make lib::meta::Types variadic - */ - template - struct TySeq - { - using Seq = TySeq; - using List = typename Types::List; - }; - - - /** - * temporary workaround: additional specialisation for the template - * `Prepend` to work also with the (alternative) variadic TySeq. - * @see typeseq-util.hpp - */ - template - struct Prepend> - { - using Seq = TySeq; - using List = typename Types::List; - }; - - - /** - * temporary workaround: strip trailing NullType entries from a - * type sequence, to make it compatible with new-style variadic - * template definitions. - * @note the result type is a TySec, to keep it apart from our - * legacy (non-variadic) lib::meta::Types - * @deprecated necessary for the transition to variadic sequences ////////////////////////////////////TICKET #987 : make lib::meta::Types variadic - */ - template - struct StripNullType; - - template - struct StripNullType> - { - using TailSeq = typename StripNullType>::Seq; - - using Seq = typename Prepend::Seq; - }; - - template - struct StripNullType> - { - using Seq = TySeq<>; // NOTE: this causes the result to be a TySeq - }; - //////////////////////////////////////////////////////////////////////////////////////////////////////////TICKET #987 temporary WORKAROUND(End) -- to be obsoleted - - - - - - - - + /* ==== Build Variadic Sequences ==== **/ diff --git a/src/lib/parse.hpp b/src/lib/parse.hpp new file mode 100644 index 000000000..1ea929043 --- /dev/null +++ b/src/lib/parse.hpp @@ -0,0 +1,388 @@ +/* + PARSE.hpp - helpers for parsing textual specifications + + Copyright (C) + 2024, Hermann Vosseler + +  **Lumiera** is free software; you can redistribute it and/or modify it +  under the terms of the GNU General Public License as published by the +  Free Software Foundation; either version 2 of the License, or (at your +  option) any later version. See the file COPYING for further details. + +*/ + + +/** @file parse.hpp + ** Convenience wrappers and definitions for parsing structured definitions. + ** Whenever a specification syntax entails nested structures, extracting contents + ** with regular expressions alone becomes tricky. Without much sophistication, a + ** directly implemented simple recursive descent parser is often less brittle and + ** easier to understand and maintain. With some helper abbreviations, notably + ** a combinator scheme to work from building blocks, a hand-written solution + ** can benefit from taking short-cuts, especially related to result bindings. + */ + + +#ifndef LIB_PARSE_H +#define LIB_PARSE_H + + +#include "lib/iter-adapter.hpp" +#include "lib/meta/function.hpp" +#include "lib/meta/trait.hpp" +#include "lib/regex.hpp" + +//#include +#include +#include + +namespace util { + namespace parse { + + using std::move; + using std::forward; + using std::optional; + using lib::meta::_Fun; + using lib::meta::has_Sig; + using lib::meta::NullType; + using std::decay_t; + using std::tuple; + + using StrView = std::string_view; + + + + /** + */ + template + struct Eval + { + using Result = RES; + optional result; + }; + + template + struct Connex + : util::NonAssign + { + using PFun = FUN; + PFun parse; + + using Result = typename _Fun::Ret::Result; + + Connex (FUN&& pFun) + : parse{move(pFun)} + { } + }; + + auto + buildConnex(NullType) + { + return Connex{[](StrView) -> Eval + { + return {NullType{}}; + }}; + } + using NulP = decltype(buildConnex (NullType())); + + auto + buildConnex (regex rex) + { + return Connex{[regEx = move(rex)] + (StrView toParse) -> Eval + { + return {matchAtStart (toParse,regEx)}; + }}; + } + using Term = decltype(buildConnex (std::declval())); + + Term + buildConnex (string const& rexDef) + { + return buildConnex (regex{rexDef}); + } + + template + auto + buildConnex (Connex const& anchor) + { + return Connex{anchor}; + } + template + auto + buildConnex (Connex && anchor) + { + return Connex{move(anchor)}; + } + + + template + auto + adaptConnex (CON&& connex, BIND&& modelBinding) + { + using R1 = typename CON::Result; + using Arg = lib::meta::_FunArg; + static_assert (std::is_constructible_v, + "Model binding must accept preceding model result."); + using AdaptedRes = typename _Fun::Ret; + return Connex{[origConnex = forward(connex) + ,binding = forward(modelBinding) + ] + (StrView toParse) -> Eval + { + auto eval = origConnex.parse (toParse); + if (eval.result) + return {binding (move (*eval.result))}; + else + return {std::nullopt}; + }}; + } + + + /* ===== building structured models ===== */ + + /** + * Product Model : results from a conjunction of parsing clauses, + * which are to be accepted in sequence, one after the other. + */ + template + struct SeqModel + : tuple + { + static constexpr size_t SIZ = sizeof...(RESULTS); + using Seq = lib::meta::TySeq; + using Tup = std::tuple; + + using Tup::Tup; + }; + + /** + * Sum Model : results from a disjunction of parsing clauses, + * which are are tested and accepted as alternatives, one at least. + */ + template + struct AltModel + { + + }; + + + /** Special case Product Model to represent iterative sequence */ + template + struct IterModel + { + + }; + + /** Marker-Tag for the result from a sub-expression, not to be joined */ + template + struct SubModel + { + + }; + + /** Standard case : combinator of two model branches */ + template class TAG, class R1, class R2 =void> + struct _Join + { + using Result = TAG; + }; + + /** Generic case : extend a structured model by further branch */ + template class TAG, class...RS, class R2> + struct _Join,R2> + { + using Result = TAG; + }; + + /** Special Case : absorb sub-expression without flattening */ + template class TAG, class R1, class R2> + struct _Join,R2> + { + using Result = TAG; + }; + template class TAG, class R1, class R2> + struct _Join> + { + using Result = TAG; + }; + template class TAG, class R1, class R2> + struct _Join,SubModel> + { + using Result = TAG; + }; + + /** Special Case : absorb further similar elements into IterModel */ + template + struct _Join, RES> + { + using Result = IterModel; + }; + + + + template + auto + sequenceConnex (C1&& connex1, C2&& connex2) + { + using R1 = typename decay_t::Result; + using R2 = typename decay_t::Result; + using ProductResult = typename _Join::Result; + using ProductEval = Eval; + return Connex{[conL = forward(connex1) + ,conR = forward(connex2) + ] + (StrView toParse) -> ProductEval + { + auto eval1 = conL.parse (toParse); + if (eval1.result) + { + uint end1 = eval1.result->length();/////////////////////////OOO pass that via Eval + StrView restInput = toParse.substr(end1); + auto eval2 = conR.parse (restInput); + if (eval2.result) + { + uint end2 = end1 + eval2.result->length(); + return ProductEval{ProductResult{move(*eval1.result) + ,move(*eval2.result)}}; + } + } + return ProductEval{std::nullopt}; + }}; + } + + + template + class Syntax; + + + template + class Parser + : public CON + { + using PFun = typename CON::PFun; + static_assert (_Fun(), "Connex must define a parse-function"); + + public: + using Connex = CON; + using Result = typename CON::Result; + +using Sigi = typename _Fun::Sig; +//lib::test::TypeDebugger buggi; +//lib::test::TypeDebugger guggi; + + static_assert (has_Sig(StrView)>() + ,"Signature of the parse-function not suitable"); + + Eval + operator() (StrView toParse) + { + return CON::parse (toParse); + } + + template + Parser (SPEC&& spec) + : CON{buildConnex (forward (spec))} + { } + +// template +// Parser (Syntax const& anchor) +// : CON{anchor} +// { } +// template +// Parser (CON const& anchor) +// : CON{anchor} +// { } + }; + + Parser(NullType) -> Parser; + Parser(regex &&) -> Parser; + Parser(regex const&) -> Parser; + Parser(string const&) -> Parser; + + template + Parser(Connex const&) -> Parser>; +// +// template +// Parser(Syntax const&) -> Parser; + + + template + class Syntax + : public Eval + { + PAR parse_; + + public: + using Connex = typename PAR::Connex; + using Result = typename PAR::Result; + + bool success() const { return bool(Syntax::result); } + bool hasResult() const { return bool(Syntax::result); } + Result& getResult() { return * Syntax::result; } + Result&& extractResult(){ return move(getResult()); } + + Syntax() + : parse_{NullType()} + { } + + explicit + Syntax (PAR&& parser) + : parse_{move (parser)} + { } + + explicit + operator bool() const + { + return success(); + } + + Syntax&& + parse (StrView toParse) + { + eval() = parse_(toParse); + return move(*this); + } + + Connex const& + getConny() const + { + return parse_; + } + + template + auto + seq (SPEC&& clauseDef) + { + return accept( + sequenceConnex (move(parse_) + ,Parser{forward (clauseDef)})); + } + + private: + Eval& + eval() + { + return *this; + } + }; + + template + auto + accept (SPEC&& clauseDef) + { + return Syntax{Parser{forward (clauseDef)}}; + } + + // template + // Parser(Syntax const&) -> Parser; + + }// namespace parse + + using parse::accept; + +}// namespace util + +namespace lib { +}// namespace lib +#endif/*LIB_PARSE_H*/ diff --git a/src/lib/regex.hpp b/src/lib/regex.hpp index ed36b9c45..76b50f37a 100644 --- a/src/lib/regex.hpp +++ b/src/lib/regex.hpp @@ -59,6 +59,24 @@ namespace util { ENABLE_USE_IN_STD_RANGE_FOR_LOOPS (RegexSearchIter); }; + + /** + * Helper algorithm to perform a search but require the match to start + * at the beginning of the string or sequence, while accepting trailing content. + * @param toParse a string, string-view or something that can be converted to string. + * @return a std::optional with the match result (`std::smatch`). + */ + template + std::optional + matchAtStart (STR&& toParse, regex const& regex) + { + auto search = RegexSearchIter{std::forward (toParse), regex}; + if (search and 0 == search->position(0)) + return *search; + else + return std::nullopt; + } + }// namespace util namespace lib { @@ -66,4 +84,4 @@ namespace lib { using std::smatch; using std::string; }// namespace lib -#endif/*LIB_STAT_REGEX_H*/ +#endif/*LIB_REGEX_H*/ diff --git a/src/steam/control/command-def.hpp b/src/steam/control/command-def.hpp index a936f95fd..d61b4c2fc 100644 --- a/src/steam/control/command-def.hpp +++ b/src/steam/control/command-def.hpp @@ -79,7 +79,6 @@ namespace control { using lib::meta::_Fun; using lib::meta::NullType; using lib::meta::Types; - using lib::meta::TySeq; using lib::meta::Tuple; diff --git a/src/steam/engine/proc-node.cpp b/src/steam/engine/proc-node.cpp index 3d91fc191..b7a3006fc 100644 --- a/src/steam/engine/proc-node.cpp +++ b/src/steam/engine/proc-node.cpp @@ -24,6 +24,7 @@ #include "lib/iter-explorer.hpp" #include "lib/format-string.hpp" #include "lib/format-util.hpp" +#include "lib/regex.hpp" #include "lib/util.hpp" #include /////////////////////////////////////////////////////TICKET #1391 is boost-hash the proper tool for this task? @@ -41,7 +42,7 @@ namespace engine { using util::contains; using boost::hash_combine; - namespace {// Details: registration and symbol table for node spec data... + namespace {// Details: parsing, registration and symbol table for node spec data... std::unordered_set procRegistry; std::unordered_set symbRegistry; diff --git a/tests/15library.tests b/tests/15library.tests index 648b9adbb..e1fc49b99 100644 --- a/tests/15library.tests +++ b/tests/15library.tests @@ -532,6 +532,11 @@ return: 0 END +TEST "helpers for specification parsing" Parse_test < + +  **Lumiera** is free software; you can redistribute it and/or modify it +  under the terms of the GNU General Public License as published by the +  Free Software Foundation; either version 2 of the License, or (at your +  option) any later version. See the file COPYING for further details. + +* *****************************************************************/ + +/** @file parse-test.cpp + ** unit test \ref Parse_test + */ + + + +#include "lib/test/run.hpp" +#include "lib/test/test-helper.hpp" +#include "lib/parse.hpp" +//#include "lib/iter-explorer.hpp" +//#include "lib/format-util.hpp" +#include "lib/meta/tuple-helper.hpp" +#include "lib/test/diagnostic-output.hpp"//////////////////TODO +//#include "lib/util.hpp" + +//#include +//#include + + + +namespace util { +namespace parse{ +namespace test { + + using lib::meta::is_Tuple; + using std::get; +// using util::join; +// using util::isnil; +// using std::vector; +// using std::shared_ptr; +// using std::make_shared; + +// using LERR_(ITER_EXHAUST); +// using LERR_(INDEX_BOUNDS); + + + namespace { // test fixture + +// const uint NUM_ELMS = 10; + +// using Numz = vector; + + } // (END)fixture + + + + + + + + /************************************************************************//** + * @test verify helpers and shortcuts for simple recursive descent parsing + * of structured data and specifications. + * + * @see parse.hpp + * @see proc-node.cpp "usage example" + */ + class Parse_test : public Test + { + + virtual void + run (Arg) + { + simpleBlah(); + acceptTerminal(); + acceptSequential(); + } + + + /** @test TODO just blah. */ + void + simpleBlah () + { + } + + /** @test TODO define a terminal symbol to match by parse. */ + void + acceptTerminal() + { + // set up a parser function to accept some token as terminal + auto parse = Parser{"hello (\\w+) world"}; + string toParse{"hello vile world of power"}; + auto eval = parse (toParse); + CHECK (eval.result); + auto res = *eval.result; + CHECK (res.ready() and not res.empty()); + CHECK (res.size() == "2"_expect ); + CHECK (res.position() == "0"_expect ); + CHECK (res.str() == "hello vile world"_expect ); + CHECK (res[1] == "vile"_expect ); + CHECK (res.suffix() == " of power"_expect ); + + auto syntax = Syntax{move (parse)}; + CHECK (not syntax.hasResult()); + syntax.parse (toParse); + CHECK (syntax.success()); + CHECK (syntax.getResult()[1] == "vile"_expect); + + // shorthand notation to start building a syntax + auto syntax2 = accept ("(\\w+) world"); + CHECK (not syntax2.hasResult()); + syntax2.parse (toParse); + CHECK (not syntax2.success()); + string bye{"cruel world"}; + syntax2.parse (bye); + CHECK (syntax2.success()); + CHECK (syntax2.getResult()[1] == "cruel"_expect); + + // going full circle: extract parser def from syntax +// using Conn = decltype(syntax2)::Connex; +// Conn conny{syntax2}; +// auto parse2 = Parser{conny}; + auto parse2 = Parser{syntax2.getConny()}; + CHECK (eval.result->str(1) == "vile"); + eval = parse2 (toParse); + CHECK (not eval.result); + eval = parse2 (bye); + CHECK (eval.result->str(1) == "cruel"); + } + + /** @test TODO define a sequence of syntax structures to match by parse. */ + void + acceptSequential() + { + auto term1 = buildConnex ("hello"); + auto term2 = buildConnex ("world"); + auto parseSeq = [&](StrView toParse) + { + using R1 = decltype(term1)::Result; + using R2 = decltype(term2)::Result; + using ProductResult = std::tuple; + using ProductEval = Eval; + auto eval1 = term1.parse (toParse); + if (eval1.result) + { + uint end1 = eval1.result->length(); + StrView restInput = toParse.substr(end1); + auto eval2 = term2.parse (restInput); + if (eval2.result) + { + uint end2 = end1 + eval2.result->length(); + return ProductEval{ProductResult{move(*eval1.result) + ,move(*eval2.result)}}; + } + } + return ProductEval{std::nullopt}; + }; + string s1{"hello millions"}; + string s2{"helloworld"}; + + auto e1 = parseSeq(s1); + CHECK (not e1.result); + auto e2 = parseSeq(s2); + CHECK ( e2.result); + + using SeqRes = std::decay_t; + CHECK (is_Tuple()); + auto& [r1,r2] = *e2.result; + CHECK (r1.str() == "hello"_expect); + CHECK (r2.str() == "world"_expect); + + auto syntax = accept("hello").seq("world"); + + CHECK (not syntax.hasResult()); + syntax.parse(s1); + CHECK (not syntax.success()); + syntax.parse(s2); + CHECK (syntax); + SeqRes seqModel = syntax.getResult(); + CHECK (get<0>(seqModel).str() == "hello"_expect); + CHECK (get<1>(seqModel).str() == "world"_expect); + } + }; + + LAUNCHER (Parse_test, "unit common"); + + +}}} // namespace util::parse::test + diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 88673074f..f9102277b 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -55132,14 +55132,819 @@ - - + + - - - + + + + + + + + + + + + + + + + +

+ matchAtStart(str, regex) : partieller Match am Anfang verankert +

+ + +
+ + + + + +

+ wichtig für Text-Parsing, wo man bestimmte Tokens akzeptieren  möchte, aber durchaus ein Rest »dahinter« übrig bleibten darf. +

+ + +
+ +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ not from first principbles... +

+

+ Es geht um eine konzise Abkürzungs-Notation für den gelegentlichen Gebrauch zum Parsen einer Spezifikation. Also um die paar wenigen Dinge, für die eine Regular-Expression nicht genügt; anders gesagt, ich möchte eine bequeme Erweiterung von Regular-Expressions um LL1-Ausdrücke. Und zwar in einer Form, die man möglichst in ein paar Definitionszeilen „mal eben hinschreiben“ kann. +

+
    +
  • + keine pfiffige Operator-Syntax, die man sich nicht merken kann +
  • +
  • + sondern eine Hülle als Builder, die die definition direkt leitet +
  • +
  • + Aufbau anhand einiger praktischer Beispiele +
  • +
  • + abschrägen bis es paßt +
  • +
+ +
+
+ + + + + + +

+ Bekanntermaßen ist die Fehlerbehandlung die Stelle, an der jedes schöne Design eines Regelsystems zusammenbricht. Deshalb kümmern wir uns erst mal um die schönen Dinge. +

+ +
+
+ + + + + + +

+ anders als in einer funktionalen Sprache stellt sich aus Sicht der imperativen Verarbeitung sofort die Frage, wie die Beweglichkeit bzw die Vielgestaltigkeit verschiedener Elementar-Parser unter ein gemeinsames Verarbeitungsschema gebracht werden kann (das dann auch noch akzeptable Kosten hat). Da ich hier vor allem auf Gelegenheits-Gebrauch ziehle, also auf eine Situation, wo beiläufig gewisse syntaktische Strukturen bezeichnet werden sollen, betone ich vor allem die Optimierbarkeit der Syntax-Definition. Das führt zu der Forderung, Indirektionen so lange wie möglich zu vermeiden oder verzögern. +

+

+ +

+

+ Also: keine std::function +

+ +
+
+ + + + + + + +

+ Es erscheint mir als Pedanterie, wenn ich nun einen extra terminal-Builder-Schritt einführen würde, nur um das "bin fertig mit der Definition" zu markieren. Dazu gibt es (cost) Variablen-Definitionen. Außerdem sehe ich schon das Thema mit den explizit konfigurierten Fehlermeldungen, und dafür würde ich noch eine separate Fehlerbehandlungs-Spec brauchen. Eben diese Art von weitverzweigtem, feingranularem Framework wollte ich vermeiden. Daher neige ich zu einem Design, in dem der Parser stateless ist, die Syntax-Spec aber stateful, weil sie dann auch gleich noch das Model enthalten kann, auf das sie ohnehin stringent getypt ist. Dafür nehme ich in Kauf, die Syntax-Definition (also die eigentlichen Parser-Combinators) mit dem Auswertungs-Zustand zu vermischen, grade auch, weil man auf diesem Weg wieder in die Definition von Sub-Klauseln einsteigen kann +

+ + +
+
+ + + + + + +

+ Wenn man schon ein zustandsbehaftetes Objekt akzeptiert, könnte auch Zuweisbarkeit bequem sein (man muß dann ja ohnehin aufpassen). Allerdings steht das im Konflikt mit dem Ansatz einer fein-granularen Typisierung, welche die Modell-Struktur abbildet. Und die Entscheidung, die recursive-descent-Struktur als parse-λ einzubetten, verbietet explizit die Zuweisbarkeit, denn man kann (und darf) nicht wissen, was in der Closure steckt +

+ + +
+
+
+ + + + + + +

+ ...das ist eine Grundentscheidung, vor allem motiviert durch die einfachere Implementierbarkeit (aber auch unterstützt dadurch, daß ich mir in vielen relevanten Fällen eine einfachere, fluidere  Verwendung erhoffe). Das hat die wichtige Konsequenz, daß wir nicht mit Variadics arbeiten, sondern die Summen- und Produkttypen schrittweise aufbauen +

+ + +
+ +
+ + + + + + +

+ Das heißt, es gibt keinen explizit gespeicherten »Binding-Funktor«, sondern stattdessen eine spezielle Kompositions-Mechanik, mit der man eine Transformation an die Parse-Funktion innerhalb des Optional  realisieren kann. Also ein monadisches Muster hier +

+ + +
+
+ + + + + + +

+ Die Parser-Kombinatoren sollen also nicht direkt vom Benutzer angefaßt werden, wodurch sie auch keiner besonderen Absicherung bedürftig sind +

+ + +
+
+ + + + + + +

+ separater Namespace sinnvoll (util::parse) +

+ + +
+ + + + + +

+ ...da für den intendierten Nutzen typischerweise Syntax-Spezifikationen bei den Basis-Konstanten und Definitionen einer anderen Einrichtung mit abgelegt werden, und in den meisten Fällen der Einstieg erfolgen kann per util::accept +

+ + +
+
+
+ + + + + + + + + + +

+ das ist mehr als eine Policy: Struktur-Bindeglied ⟹ Connex +

+ +
+
+
+ + + + + + + + + + +

+ das ist essentiell für den Vorgang des Parsens: es wird jeweils ein Präfix-Match gesucht, dann »akzeptiert« und mit dem Rest dahinter weitergemacht — wobei allerdings sich dieser »Rest« erst durch den Match überhaupt definiert. Leider bieten die Regex-Operationen nur ein find (mit beliebigem Match irgendwo) oder match (auf die ganze Sequenz). +

+ +
+
+ + + + + + +

+ das löst das Problem, ist aber keine gute Lösung; denn das ist ein subtiler Punkt, den man dem User überlassen muß. Man könnte noch versuchen, diese Verankerung am Anfang automatisch mit dazuzubauen, was aber nur geht, wenn die Regular-Expression als String-Definition geliefert wird. Also ungeschickt wie man's dreht +

+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +

+ ...woduch der Code ziemlich verwirrend wird  ⟹  idealerweise wäre die gesamte Combinator-Logik in den buildConnex()-Overloads +

+ + +
+
+ + + + + + + + + + + + + + + + +

+ ist viel näher an der Grundidee aus der Funktionalen Programmierung, und dennoch in einer low-Level-Funktion, mit der man direkt nix anfangen kann. Denn darauf läuft das Design ja immer mehr hinaus: ein eingekappseltes Kombinator-Framework. +

+ + +
+
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ wenn das Vorgänger-Element eine passende Model-Variante ist (tuple, AltTypes, array), dann wird an dieses angebaut +

+ + +
+
+ + + + + + + + +

+ Angenommen, die Syntax sieht wie folgt aus: +

+

+ '(' num '+' num ')' '*' num +

+

+ Wenn der geklammerte Ausdruck als sub-Syntax formuliert ist, würden alle Model-Elemente in ein einziges 7-Tupel nivelliert, obwohl man eingentlich ein 3-Tupel mit verschachtelung wollte +

+ + +
+
+ + + + + +
+ + + + + + + +

+ ...da wir keinen expliziten Binding-Funktor speichern, sondern ihn in die Parse-Funktion einarbeiten, kann auch der Ergebnis-Typ durch eine weitere solche Komposition geLIFTet werden. +

+ + +
+
+ + + + + + + + + + + + + +

+ Das heißt, die Operation zur inkrementellen Erweierung erkennt den gruppierenden Summen/Produkttyp im Parse-Ergebnis und wird demenstprechend, »Klammer schließen« oder »anhängen«. In der Implementierung der Verknüpfungs-Operation liegt also eine Fallunterscheidung oder ein double-Dispatch vor, analog zum Pattern-Match in der funktionalen Programmierung. Soweit so gut. +

+ + +
+
+
+ + + + + + + + + + + +

+ Beispiel +

+

+ +

+

+   accept( +

+

+      accept("x").seq("y") +

+

+   ).seq("z") +

+ + +
+
+ + + + + + +

+ ⟹ wird zu... +

+

+ +

+

+   Syntax( 'x' 'y' | tuple<Model(x), Model(y)> ) +

+

+       .seq("z") +

+ + +
+
+ + + + + + +

+ ⟹ wird zu... +

+

+ +

+

+   Syntax( 'x' 'y' 'z' | tuple<Model(x), Model(y), Model(z)> ) +

+ + +
+
+ + + + + + +

+ ⟺ äquivalent zu... +

+

+ +

+

+   accept("x") +

+

+      .seq("y") +

+

+      .seq("z") +

+ + +
+
+
+ + + + + + + + + + + + + + + + +

+ ...denn das wäre nahe an der Stelle, wo's zur Erkennung gebraucht wird, und ist damit auch ziemlich tief in der Implementierung verborgen +

+ + +
+
+ + + + + + +

+ will sagen, wer ein solches zweimal verschachteltes accept(accept()) anfängt, und dann nicht in einen Produkt / Summenterm einsteigt, ist selber schuld +

+ + +
+
+ + + + + + +

+ man könnte es sogar nur im Result-Typ verstecken +

+ + +
+ + + + + +

+ ...was zwar auch etwas grenzwertig wäre, aber noch vertretbar, wenn es als eine Tagging-Subklasse realisiert ist. Dann ist zwar die Bedingung gebrochen, daß Connex::Result ≡ parse-Funktion-Result, aber ersteres wäre eine Subklasse und somit noch kompatibel +

+ + +
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + +

+ Ansatz: ModelJoin<R1,R2>::Result +

+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ nebenbei: namespace parse einführen +

+ + +
+
+
+
+ + + +
+
+ + + +
+
+ + + + + + + + + + +
+ + + + + + + + + + +
+ + +
+ + + + + + + + + + + + + + + + + + @@ -99793,7 +100598,7 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + @@ -102086,6 +102891,28 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + + + + + + + + + + + +

+ ...also weiß, daß man nicht mutwillig mit Whitespace herumspielt, daß man eine stabile Spec am Besten generiert, daß man Unicode-Sonderzeichen vermeidet und sich generell um die Eindeutigeit der Syntax kümmert, selbst wenn einem niemand auf die Finger schaut +

+ + +
+
+
@@ -102146,7 +102973,34 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - + + + + + + + +

+ eine ID der Domain-Anbindung / Domain-Ontology / Library +

+ + +
+
+ + + + + + +

+ high-Level-Beschreibung der Funktionalität dieser Node ... steht wahrscheinlich in Beziehung zur Asset-ID +

+ + +
+
+
@@ -102259,6 +103113,173 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) + + + + + + + + + + + + +

+ Begründung: die Schwachstelle jedes Parsers liegt bei den Anknüpfungen. Eine Library wird unvermeidbar eine Objekt- oder Baumnotation mit sich bringen, und sei es bloß eine Konvention auf Basis von std::tuple. Eine gut ausgereifte und weit verbreitete Library wird vermutlich hier eine schwergewichtige Lösung bieten; rein nach Bauchgefühl erwarte ich, daß eine solche Lösung aufgebläht, schlecht in der Performence, extrem tricky, schwer lesbar ist, oder uns ungünstige Verhaltensmuster aufzwingt (Monaden!). +

+ + +
+ +
+ + + + + + +

+ Demgegenüber erscheint mir eine minimalistische ad-hoc-Lösung viel attraktiver, sofern sie auf dem Niveau von »ein paar Abkürzungs-Notationen« bleibt. Das heißt, sie sollte die Mechanik des Parsens nicht zu verbergen versuchen. +

+

+ +

+

+ Maßstab ist für mich der bereits etablierte Gebrauch von Regular-Expressions (header regexp.hpp): Im Grunde haben wir nur noch eine convenience-Verpackung eines Iterators, ein paar, Typ-Abkürzungen und Imports, sowie den Stil, jeweils im Implementierungs-Header die Grammatik in Form von Reg-Exp-Bausteienen zu definieren. Etwas Vergleichbares sollte auch für Parser-Kombinatoren möglich sein, wenn man sich diesem Gebrauch in mehreren Schritten nähert (und dabei Erfahrungen sammelt) +

+ + +
+
+ + + + + + +

+ man könnte eine klammern-zählende Hilfsfunktion schreiben und dann den Inhalt der Klammern per RegExp zerlegen +

+ + +
+
+
+ + + + + + + + +

+ Zielvorgabe: im Grunde zu Fuß machen (mit Abkürzugen) +

+ + +
+ +
+ + + + + + + + +

+ ...weil ich eigentlich keinen Aufwand in sowas stecken sollte, aber auch keine Bastel-Lösung an einer so zentralen Stelle möchte, und jetzt schon wieder zwei Tage daran herumprokrastiniere und den inneren Konflikt nicht lösen kann. Ja, es reizt mich, es besser zu lösen als all die Libraries, die ich gesehen habe und nicht im Projekt haben möchte. Und »Pragmatismus« empfinde ich als Kränkung und Niderlage hier +

+ + +
+ + +
+
+
+
+ + + + + + + + +

+ siehe text-template.cpp +

+ + +
+ + + + + + + +

+ der Aufwand zum Parsen eines gequoteten CSV ist im Regelfall kaum höher als die stupide Zerlegung an einem Escape-Zeichen; dafür aber ist die Notation als kommaseparierte Liste sehr intuitiv, was Testen und Diagnose erleichtern wird. Quotes müssen wir dann aber vorsehen, weil komplexe Typen möglicherweise Kommata enthalten können, und ich diese Tür nicht sofort schließen möchte (zwar wird es vermutlich darauf hinauslaufen, daß die Library-Binding-Plug-ins ihre eigenen, synthetischen Typ-Bezeichner einführen) +

+ + +
+
+ + + + + + + + + + + +

+ wir brauchen hier definitiv keinen Stack, sondern nur einen internen count-down +

+ + +
+ +
+
+ + + + + + +

+ dafür wurde dieser Mechanismus ja grade geschaffen, und das State-Core-API ist leicht zu dekorieren +

+ + +
+
+
+
+ + + + + + +

+ die Normalisierung hier nicht zu weit treiben; es ist ein low-Level-Interface und dient in 99% der Fälle dazu, Hash-Keys zu erzeugen +

+ + +
+ +
+
@@ -102419,17 +103440,25 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + - - + + + + + + + + + + @@ -103147,8 +104176,8 @@ StM_bind(Builder<R1> b1, Extension<R1,R2> extension) - - + + @@ -144344,6 +145373,11 @@ std::cout << tmpl.render({"what", "World"}) << s + + + + + @@ -150693,6 +151727,174 @@ unsigned int ThreadIdAsInt = *static_cast<unsigned int*>(static_cast<vo + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ sondern verwenden einen Parser-Builder +

+ +
+ + + + + + + + + + + + +

+ ein sub-Parser wird akzeptiert, aber es ist kein Fehler wenn er übrhaupt nicht greift; partielle Anwendbarkeit aber ist ein Fehler +

+ +
+
+ + + + + + +

+ repetitive Strukturen +

+
    +
  • + Kleene-* +
  • +
  • + Kleene-+ +
  • +
  • + delimited List +
  • +
+ +
+
+
+
+
+ + + + + + +

+ als Teil der Kombination wird auch Kombination der Ergebnisse  gehandhabt +

+ +
+ + + + + + +
+ + + + + + + + + + + + + +

+ Dazu gehören schon die sehr geläufigen Bausteine wie optional und sequenced-by. Mit erweiterter Vorschau und Modell-Interaktion ist man schnell nicht mehr kontextfrei. +

+ +
+
+ + + + + + +

+ Hier gibt es mehrere Faktoren zu beachten +

+
    +
  • + die Eigenschaften der Sprache: wie leicht kann ein vom Parser erzeugtes Modell anschließend ausgewertet werden? +
  • +
  • + die Umstände der Anwendung: welche Struktur hat die zu gewinnende Information? Syntaxbaum, Steuerprädikat, strukturierte Daten, Befehlsvarianten,.... +
  • +
  • + die Eindeutigkeit der Grammatik: mit Parser-Kombinatoren schafft man leicht fragile, hochgradig zweideutige Grammatik-Strukturen +
  • +
  • + die Art der Konfigurierbarkeit: starr mit festen Regeln für einen Ergebnisbaum vs. flexibel mit lokaler konfigurierbarkeit oder impliziter Logik +
  • +
  • + Umgang mit Backtracking, welches aus Alternativen in der Grammatik resultiert; wie gut ist die Ergebnis-Repräsentation im Stande, letztlich nicht erfüllbare Hypothesen wieder zurückzurollen? +
  • +
+ +
+
+ + + + + + +

+ die schöne Formulierung der Produktionen in der DSL täuscht meist darüber hinweg, daß es oft schwer verständlich ist, was der Parser-Code eigentlich macht, besonders wenn implizite Konventionen involviert sind. Fehlerbehandlung ist so schwer und mühsam wie stets im Parserbau, aber Fehlerbehandlung zusammen mit spezieller Steuerlogik in den Anknüpfungen führt schnell zu nahezu unwartbarem Code. Aber auch im anderen Extrem, bei einem Framework welches lediglich den Syntaxbaum als Algebraic-Type liefert, ist die dann folgende, eigentliche Auswertung per Tree-Walk oft extrem schwer zu verstehen, da man die gesamte Grammatik im Kopf haben muß +

+ +
+
+ + + + + + +

+ Der einzelne Parser im zusammengesetzten Parser-Ausdruck ist opaque und daher nicht der Optimierung zugänglich. Anders als für einen LR-Parser können verschiedene Alternativen nicht so einfach zugleich verfolgt werden. Dadurch entsteht viel Aufwand durch prüfen und Backtracking von Alternativen und die Kosten wachsen u.U. exponentiell in der Läge der Eingabe oder Umfang der Grammatik. Hinzu kommt, daß für ein Modell oft flexible Meta-Strukturen geschaffen werden müssen, nur um sie nach Traversierung wieder zu verwerfen +

+ +
+
+
+
+