From ed5c6f7c17ac1468e39b94e124f5a46d7186df32 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Sun, 26 Jan 2025 23:54:38 +0100 Subject: [PATCH] Library: implement support for recursive syntax The concept was indeed successful, albeit quite difficult to pull off in detail. It requires a carefully crafted path of Deduction guides and overloads to effect the switch from std::function to std::function& at the point where a predeclared syntax clause placeholder is used recursively --- src/lib/parse.hpp | 88 ++++++++-- tests/library/parse-test.cpp | 20 ++- wiki/thinkPad.ichthyo.mm | 305 +++++++++++++++++++++-------------- 3 files changed, 279 insertions(+), 134 deletions(-) diff --git a/src/lib/parse.hpp b/src/lib/parse.hpp index e8f75d310..0d145141d 100644 --- a/src/lib/parse.hpp +++ b/src/lib/parse.hpp @@ -70,6 +70,9 @@ namespace util { template class Syntax; + template + class Parser; + /** @@ -97,10 +100,20 @@ namespace util { using Result = typename _Fun::Ret::Result; Connex (FUN pFun) - : parse{move(pFun)} + : parse{pFun} { } }; + /** special setup to be pre-declared and then used recursively */ + template + using OpaqueConnex = Connex(StrView)>>; + + template + using ForwardConnex = Connex(StrView)>&>; + + + + /** »Null-Connex« which always successfully accepts the empty sequence */ auto @@ -166,6 +179,18 @@ namespace util { return Con{anchor}; } + /** special setup to attach to a pre-defined clause for recursive syntax + * @note works in concert with the Parser deduction guide, so that the + * resulting ForwardConnex holds a _reference_ to std::function, + * and thus gets to see the full definition reassigned later. */ + template + auto + buildConnex (Syntax>> & refClause) + { + OpaqueConnex& refConnex = refClause; + return ForwardConnex{refConnex.parse}; + } + namespace { @@ -182,6 +207,17 @@ namespace util { { // probe the λ with ARG to force template instantiation using Ret = decltype(std::declval() (std::declval())); }; + + + template + inline bool + _boundFun(FUN const& fun) + { + if constexpr (std::is_constructible()) + return bool(fun); + else + return std::is_invocable(); + } } /** @@ -580,6 +616,7 @@ namespace util { Eval operator() (StrView toParse) { + REQUIRE (_boundFun (CON::parse), "unbound recursive syntax"); return CON::parse (toParse); } @@ -596,10 +633,14 @@ namespace util { Parser(string const&) -> Parser; template - Parser(Connex const&) -> Parser>; + Parser(Connex) -> Parser>; template - Parser(Syntax const&) -> Parser; + Parser(Syntax) -> Parser; + + template + Parser(Syntax>>) -> Parser>; + // bind to recursive syntax by reference /** @internal meta-helper : detect if parser can be built from a given type */ @@ -658,11 +699,12 @@ namespace util { operator Connex&() { return parse_; } operator Connex const&() const { return parse_; } - bool success() const { return bool(Syntax::result); } - bool hasResult() const { return bool(Syntax::result); } - size_t consumed() const { return Eval::consumed;} - Result& getResult() { return * Syntax::result; } - Result&& extractResult() { return move(getResult()); } + bool success() const { return bool(Syntax::result); } + bool hasResult() const { return bool(Syntax::result); } + bool canInvoke() const { return _boundFun(parse_.parse);} + size_t consumed() const { return Eval::consumed; } + Result& getResult() { return * Syntax::result; } + Result&& extractResult() { return move(getResult()); } /********************************************//** @@ -676,6 +718,16 @@ namespace util { } + template + Syntax& + operator= (Syntax refSyntax) + { + using ConX = typename PX::Connex; + ConX& refConnex = refSyntax; + parse_.parse = move(refConnex.parse); + return *this; + } + /** ===== Syntax clause builder DSL ===== */ @@ -878,7 +930,23 @@ namespace util { } - /** Setup an assignable, recursive Syntax clause, initially empty */ + /** + * Setup an assignable, recursive Syntax clause, initially empty. + * @remark this provides the foundation for recursive syntax clauses; + * initially, an empty std::function with the pre-declared return + * type is embedded. Together with a special Parser deduction guide, + * later on a full syntax clause can be built, taking a _reference_ + * to this function; finally the definition prepared here should be + * _re-assigned_ with the fully defined syntax, which is handled + * by the assignment operator in class Syntax to re-assign a + * working parser function into the std::function holder. + * @tparam RES the result model type to be expected; it is necessary + * to augment the full definition explicitly by a model-binding + * to produce this type — which typically also involves writing + * actual code to deal with the possibly open structure enable + * through a recursive syntax definition + * @see Parse_test::verify_recursiveSyntax() + */ template auto expectResult() @@ -911,7 +979,7 @@ namespace util { * So either the already defined part of this Syntax matches the input, * or the alternative clause is probed from the start of the input. At least * one branch must match for the parse to be successful; however, further - * branches are not tested after finding a matching branch (short-circuit). + * branches are not tested after finding a matching branch (short-circuit). * The result model is a _Sum Type,_ implemented as a custom variant record * of type \ref SubModel. It provides a branch selector field to detect which * branch of the syntax did match. And it allows to retrieve the result model diff --git a/tests/library/parse-test.cpp b/tests/library/parse-test.cpp index 4738fe732..52f3115d4 100644 --- a/tests/library/parse-test.cpp +++ b/tests/library/parse-test.cpp @@ -624,9 +624,27 @@ namespace test { void verify_recursiveSyntax() { - auto recursive = expectResult(); + auto recurse = expectResult(); + CHECK (not recurse.canInvoke()); + + recurse = accept("great") + .opt(accept("!") + .seq(recurse)) + .bind([](auto m) -> int + { + auto& [_,r] = m; + return 1 + (r? get<1>(*r):0); + }); + CHECK (recurse.canInvoke()); string s1{"great ! great ! great"}; + recurse.parse(s1); + CHECK (recurse.success()); + CHECK (recurse.getResult() == 3 ); + + CHECK (not recurse.parse(" ! great")); + CHECK (recurse.parse("great ! great actor").getResult() == 2); + CHECK (recurse.parse("great ! great ! actor").getResult() == 2); } }; diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 6d32f1223..69efc7a2d 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -56776,7 +56776,7 @@ - + @@ -56808,7 +56808,7 @@ - + @@ -56854,7 +56854,7 @@ - + @@ -56934,7 +56934,7 @@ - + @@ -57023,9 +57023,7 @@ - - - +

nebenbei abgefallen @@ -57041,9 +57039,7 @@ - - - +

das muß tatsächlich ein Postfix-Operator sein @@ -57055,9 +57051,7 @@ - - - +

Vorschlag: bind(FUN) @@ -57066,11 +57060,9 @@ - + - - - +

damit klar gesagt wird, wenn die Funktion nicht den bisherigen Result-Typ nimmt @@ -57105,9 +57097,7 @@ - - - +

Standard-Variante: bindMatch(n) @@ -57116,13 +57106,11 @@ - + - - - +

...denn es steuert die Art der Dekoration @@ -57146,9 +57134,7 @@ - - - +

analog wie die anderen Combinatoren und buildConnex() @@ -57163,9 +57149,7 @@ - - - +

nachdem ich die Model-Fälle wegdiskutiert habe ☺ @@ -57182,10 +57166,14 @@ - - + + - + + + + + @@ -57197,9 +57185,7 @@ - - - +

es bräuchte für alle erdenklichen Fälle einen Pfad, um auf einen String zu kommen; also bräuchte es sowas wie einen operator string(), oder man müßte rekursiv in alle Teilkomponenten hinein mappen; und was dann mit Komponenten, die bereits explizit transformiert wurden, wie erkennt man die, und was macht man mit denen?? @@ -57210,9 +57196,7 @@ - - - +

wozu will man das? doch nur für Tests. @@ -57245,9 +57229,7 @@ - - - +

Die C++ »structured bindings« funktionieren für Arrays, für tuple-like  und aber auch für einfache PODs. Wenn std::tuple_size ein incomplete-type  ist, dann versucht der Compiler ein Binding auf Struct-Felder, scheitert aber daran, daß es eine nicht-triviale Basis-Klasse gibt (und damit die Feld-Nummer nicht mehr offensichtlich klar ist) @@ -57273,9 +57255,7 @@ - - - +

ganz banal: habe eval.consumed nicht weitergegeben @@ -57284,9 +57264,7 @@ - - - +

d.h. jede sub-expression setzt wieder am Anfang auf @@ -57306,27 +57284,35 @@ - + + - - + + + + - + + + + + + - - - - + + + + + + - - - +

....weil eine rekursive Definition im Prinzip offen ist und im Extremfall auch tatsächlich nicht terminiert; in Haskell könnte man einen solchen Typ anschreiben, in C++ nicht (weil Typ-Ausdrücke eager aufgelöst werden) @@ -57348,10 +57334,10 @@ - + - + @@ -57379,16 +57365,17 @@ - + + - + - + @@ -57413,8 +57400,8 @@ - - + + @@ -57465,7 +57452,7 @@ - + @@ -57480,7 +57467,7 @@ - + @@ -57488,7 +57475,8 @@ - + + @@ -57527,9 +57515,7 @@ - - - +

diese andere Syntax hätte dann aber auch einen anderen Typ und müßte in einer anderen Syntax-Variablen gespeichert werden; @@ -57551,9 +57537,7 @@ - - - +

Variante-1 ist »filosofisch« und praktsich attraktiv @@ -57562,9 +57546,7 @@ - - - +

...weil sie der „dann mach's halt nicht falsch“-Haltung entspricht, die diesem ganzen Parser-Framework zugrunde gelegt wurde; und ganz praktisch: man bekommt diese Variante geschenkt, alles funktioniert von selber so wie es soll — und wenn irgendjemand unbedingt dekorieren möchte, dann soll er halt @@ -57575,9 +57557,7 @@ - - - +

Weil das, was man nun zusätzlich machen könnte, nur auf Basis der Implementierung verständlich ist, aber für jeden Benutzer ziemlich verwirrend @@ -57587,14 +57567,13 @@ - + + - - - +

da sie ohnehin das erlaubt was man machen sollte, aber Fehl-Verwendungen unterbindet @@ -57605,9 +57584,7 @@ - - - +

da man ja dennoch irgendwie auf diese Funktion Bezug nehmen kann, indem man sie in andere Sytnax einbaut, ist die Abgrenzung zum »Dekorieren«  nicht klar @@ -57620,14 +57597,12 @@ - + - - - +

...sie bestünde darin, die Referenzen nach der Zuweisung zu materialisieren;  aber die Schwierigkeit besteht darin dieses Linken auszulösen, da die ganze DSL darauf abstellt, Funktionen beliebig ineinander zu verschachteln, und damit sehr viel zu kopieren; man müßte dann entweder eine komplette Link-Infrastruktur hochziehen (Parser-Funktionen wären speziell als noch ungelinkt markiert und es gäbe einen separaten Call-Chain), oder man müßt das Binden/Materialisieren beim ersten Aufruf machen, was in der Praxis nicht sonderlich hilfreich ist @@ -57643,9 +57618,9 @@ - - - + + + @@ -57653,35 +57628,34 @@ - + + - - + + - + - - - +

FUN ≡ std::function &

- -
+ +
+ +
- + - - - +

Connex-Definition @@ -57690,27 +57664,25 @@ erfüllt das bereits

- -
+ - + - + - - - - + + + + + - - - +

da viele Builder-Funktionen in ein neues Syntax-Objekt schieben @@ -57729,16 +57701,95 @@ - + + + + + - - - + + + + + + + + + + + + + + + + + + + + +

+ es sei denn, man würde Connex komplett aufdoppeln für den Referenz-Fall (wobei ärgerlicherweise wirklich aller Code identisch wäre, bis auf eine Variante im Konstruktor). Natürlich habe ich auch versucht, nur den Konstruktor allein zu spezialisieren, das ist mir aber nicht gelungen +

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

+ ausdefinierte Syntax per Value (!) nehmen +

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

+ ...es fällt mir schwer, keine Fehler-Checks zu machen, aber ein kurzer Versuch zeigt, daß diese so einiges Metaprogramming erfordern würden — und ich habe generell beschlossen, hier keine Parser-Library zu entwickeln, sondern nur Abkürzungen für einfache Parse-Tasks,  die jemand wie ich auch von Hand (per Rekursive-descent) schreiben könnte. In dem Fall bin ich also mal arrogant und warte, was passiert, denn ich kann mir nicht vorstellen, daß man dieses Framework ohne gewisse Erfahrungen mit Parsern verwenden kann... +

+ + +
+
+
+
+ + + + + + + + + + @@ -57801,6 +57852,14 @@ + + + + + + + +