From 860e2fa22607c0c3e9f6f83c983aa33f9dde23d7 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Sat, 25 Jan 2025 02:48:11 +0100 Subject: [PATCH] Library: investigate how to approach recursion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allowing free recursion in grammars is the key enabling feature, which allows to accept arbitrary complex structures (like numeric expressions). It is however also the element which makes the task of parsing a challenging endeavour; after weighting the arguments, I decided ''not to place the focus on advanced usage,'' yet to open a pathway towards representation of such grammars. Essentially, I consider it acceptable to require some additional work by the user, if arbitrary recursive grammars are desired; because this design relies on explicitly given parse functions, we need to introduce some kind of indirection interface, to allow ''declaring'' a recursive rule first and later to ''supply the definition,'' which obviously then will involve other rules (or itself) recursively. This leads to a very ''nifty approach'' towards recursion: we require the user to provide an ''explicit model type'' beforehand, which implies that this is a simple type, that can be spelled out (no λ) — and so the user is also ''forced to augment the actual rule with a model-binding,'' thereby reducing the structured return types from the parse into something simple and uniform. The user ''has to do the hard work,'' but can ''exploit additional knowledge'' related to the specific use case. All this framework needs to do then is to supply a `std::function`, using the explicit return type given; everything else will still work as implemented, since a `std::function` can always stand-in for any arbitrary λ. --- src/lib/parse.hpp | 61 ++++- tests/library/parse-test.cpp | 32 ++- wiki/thinkPad.ichthyo.mm | 451 ++++++++++++++++++++++++++++++++++- 3 files changed, 533 insertions(+), 11 deletions(-) diff --git a/src/lib/parse.hpp b/src/lib/parse.hpp index 19b393ae8..b77aaa781 100644 --- a/src/lib/parse.hpp +++ b/src/lib/parse.hpp @@ -20,6 +20,13 @@ ** 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. + ** + ** So what is provided here is _not a parser library_ — yet aims at »making + ** simple things simple« and let you implement the complicated ones yourselves. + ** Several decisions were taken accordingly, like only supporting std::string_view + ** and automatically consuming any leading whitespace. And notably the focus was + ** _not placed_ on the challenging aspects of parsing — while still allowing a + ** pathway towards definition of arbitrarily recursive grammars, if so desired. */ @@ -160,6 +167,25 @@ namespace util { } + +// namespace { +// /** handle all regular "function-like" entities */ +// template +// struct FunDetector +// { +// using Sig = typename _Fun::Sig; +// }; +// +// /** handle a generic lambda, accepting a reference to the `SRC` iterator */ +// template +// struct FunDetector> > +// { +// using Arg = std::add_lvalue_reference_t; +// using Ret = decltype(std::declval() (std::declval())); +// using Sig = Ret(Arg); +// }; +// } + /** * Adapt by applying a result-transforming function after a successful parse. * @remark the purpose is to extract a custom data model immediately from the @@ -171,10 +197,12 @@ namespace util { adaptConnex (CON&& connex, BIND&& modelBinding) { using RX = 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; +// using Arg = lib::meta::_FunArg; +// static_assert (std::is_constructible_v, +// "Model binding must accept preceding model result."); +// using AdaptedRes = typename _Fun::Ret; + using Arg = std::add_lvalue_reference_t; + using AdaptedRes = decltype(modelBinding (std::declval() )); return Connex{[origConnex = forward(connex) ,binding = forward(modelBinding) ] @@ -663,6 +691,11 @@ namespace util { template auto bracketOpt (SPEC&& bodyDef); + template + auto bind (FUN&& modelAdapt); + + auto bindMatch (uint group =0); + private: Eval& eval() { return *this;} }; @@ -966,6 +999,26 @@ namespace util { return seq (accept_bracketOpt (forward(bodyDef))); } + template + template + auto + Syntax::bind (FUN&& modelAdapt) + { + return accept( + adaptConnex (move(parse_) + ,forward(modelAdapt))); + } + + template + auto + Syntax::bindMatch (uint group) + { + return bind ([group](smatch const& mat) + { + return mat.str(group); + }); + } + }// namespace parse diff --git a/tests/library/parse-test.cpp b/tests/library/parse-test.cpp index 799327368..4202ade69 100644 --- a/tests/library/parse-test.cpp +++ b/tests/library/parse-test.cpp @@ -76,19 +76,21 @@ namespace test { virtual void run (Arg) { - simpleBlah(); + simpleUsage(); acceptTerminal(); acceptSequential(); acceptAlternatives(); acceptIterWithDelim(); acceptOptionally(); acceptBracketed(); + + verify_modelBinding(); } /** @test TODO just blah. */ void - simpleBlah () + simpleUsage () { } @@ -559,6 +561,32 @@ namespace test { CHECK (not accept_bracket("a","n","...").parse(" gain")); // opening expression "a" missing CHECK (not accept_bracket("a","n", word).parse("again")); // "\\w+" consumes eagerly => closing expression not found } + + + + /** @test define syntax with bracketed sub-expressions */ + void + verify_modelBinding() + { + auto word{"\\w+"}; + using Mod1 = SeqModel; + auto syntax1 = accept(word).seq(word) + .bind([](Mod1 res) + { +// auto& [a,b] = res; + return res.get<0>().str() +"-"+ res.get<1>().str(); + }); + + string s1{"ham actor"}; + CHECK (not syntax1.hasResult()); + syntax1.parse(s1); + CHECK (syntax1.success()); + auto res1 = syntax1.getResult(); +SHOW_TYPE(decltype(res1)) + CHECK (showType() == "string"); +SHOW_EXPR(res1) + CHECK (res == "ham-actor"_expect); + } }; LAUNCHER (Parse_test, "unit common"); diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index af2ac61c5..4d7f91f35 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -56995,8 +56995,7 @@ ....muß dann aber durch die Entscheidungs-Logik sicherstellen, daß dann auch die zugehörige schließende Klammer entweder erwartet, oder übersprungen wird.

- - + @@ -57013,12 +57012,121 @@ - + - + + + + + + + + + + +

+ nebenbei abgefallen +

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

+ das muß tatsächlich ein Postfix-Operator sein +

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

+ Vorschlag: bind(FUN) +

+ + +
+
+ + + + + + +

+ damit klar gesagt wird, wenn die Funktion nicht den bisherigen Result-Typ nimmt +

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

+ Standard-Variante: bindMatch(n) +

+ + +
+ +
+
+ + + + + + + + + + + + + + @@ -57031,6 +57139,331 @@ + + + + + + + + + + +

+ ....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) +

+ + +
+
+ + + + + + +

+ ...denn was wir abschneiden ist nur die komplett ausformulierte Struktur des Typs; an der Stelle, an der eine andere Klausel rekursiv eingebunden wird, greifen wir nur deren Ergebnis-Model auf. +

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

+ der Lambdas wegen +

+ + +
+
+ + + + + + +

+ das heißt, es würde eine Art late-Binding notwendig +

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

+ und später an diese zuweisen +

+ + +
+ + + + + + +

+ das impliziert Heap-Storage für das Parse-λ +

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

+ ...denn sonst dürfte es kaum möglich sein, einen explizit angebbaren Result-Typ zu konstituieren; in diesem Result-Binding steckt der eigentliche Ansatz, mit dem eine offen-rekursive Grammatik dennoch handhabbar wird, denn es muß eine Art Reduktion der Komplexität erfolgen, beispielsweise indem sofort ein Ergebnis ausgerechnet wird +

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

+ Konsequenz ⟹ das Einbinden in andere Syntax ist speziell zu behandeln +

+ + +
+ + + + + +

+ ....weil ja das Einbinden technisch nichts anderes ist, als eine Dekoration (und damit unterbunden würde) +

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

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

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

+ Variante-1 ist »filosofisch« und praktsich attraktiv +

+ + +
+ + + + + +

+ ...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 +

+ + +
+
+ + + + + + +

+ 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 +

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

+ da sie ohnehin das erlaubt was man machen sollte, aber Fehl-Verwendungen unterbindet +

+ + +
+
+ + + + + + +

+ 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 +

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

+ ...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 +

+ + +
+
+ + + + +
+
+
@@ -57076,13 +57509,21 @@
- + + + + + + + + +