From f8d0c1cf0b3882882c9471409304d918afae3b08 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Tue, 28 Jan 2025 20:23:28 +0100 Subject: [PATCH] =?UTF-8?q?Library:=20demonstrate=20=C2=BBthe=C2=AB=20text?= =?UTF-8?q?book=20example?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ...evaluating the recursive syntax of a numerical expression! * so this light-weight parsing support framework indeed allows to build fully capable LL(x) parsers, when the user knows how handle syntax clauses and bind the result models * furthermore, a notation is demonstrated how to arrange the binding functions so to keep the syntax definition legible * this involves a shortcut for homogeneous alternatives --- src/lib/branch-case.hpp | 9 ++ src/lib/parse.hpp | 2 +- tests/library/parse-test.cpp | 70 ++++++++++- wiki/thinkPad.ichthyo.mm | 236 ++++++++++++++++++++--------------- 4 files changed, 208 insertions(+), 109 deletions(-) diff --git a/src/lib/branch-case.hpp b/src/lib/branch-case.hpp index 50a0b7246..9bef475ab 100644 --- a/src/lib/branch-case.hpp +++ b/src/lib/branch-case.hpp @@ -263,6 +263,15 @@ namespace lib { { return access>(); } + + /** access the selected value of a homogeneous model. + * @warning compiles only if all cases yield the same type + */ + auto + getAny() + { + return accept([](auto val){ return val; }); + } }; }// namespace lib diff --git a/src/lib/parse.hpp b/src/lib/parse.hpp index 0d145141d..9295c8905 100644 --- a/src/lib/parse.hpp +++ b/src/lib/parse.hpp @@ -296,7 +296,7 @@ namespace util { template SeqModel (SeqModel&& seq, XX&& extraElm) : Tup{std::tuple_cat (seq.extractTuple() - ,make_tuple (forward (extraElm)) )} + ,std::make_tuple (forward (extraElm)) )} { } template diff --git a/tests/library/parse-test.cpp b/tests/library/parse-test.cpp index 52f3115d4..fd852c780 100644 --- a/tests/library/parse-test.cpp +++ b/tests/library/parse-test.cpp @@ -105,7 +105,7 @@ namespace test { string toParse{"hello vile world of power"}; auto eval = parse (toParse); CHECK (eval.result); - auto res = *eval.result; // ◁——————————— the »result model« of a terminal parse is the RegExp-Matcher + smatch res = *eval.result; // ◁——————————— the »result model« of a terminal parse is the RegExp-Matcher CHECK (res.ready() and not res.empty()); CHECK (res.size() == "2"_expect ); CHECK (res.position() == "0"_expect ); @@ -271,6 +271,17 @@ namespace test { CHECK (res.get<1>() == "seduced"); + // AltModel with homogeneous types are special + auto hom = AltModel::mark_right(42); + CHECK (hom.getAny() == 42); + CHECK (hom.selected() == 1 ); + + hom = AltModel::mark_left(55); + CHECK (hom.getAny() == 55); + CHECK (hom.selected() == 0 ); + + + //_____________________________________________ // Demonstration: how branch combinator works.... auto term1 = buildConnex ("brazen"); @@ -619,7 +630,16 @@ namespace test { } + /** @test definition of recursive Syntax clauses + * - pre-declared placeholder with known result + * - bind a syntax clause later to that placeholder, + * which is possibly only with a binding to yield + * the expected result type; in the example here + * we count the optional sequenced expressions + * - demonstrate textbook example of nested numeric + * expression, including parentheses and even a + * square root function. Calculate golden ratio! */ void verify_recursiveSyntax() @@ -637,14 +657,56 @@ namespace test { }); CHECK (recurse.canInvoke()); - string s1{"great ! great ! great"}; - recurse.parse(s1); + recurse.parse("great ! great ! great"); CHECK (recurse.success()); CHECK (recurse.getResult() == 3 ); - CHECK (not recurse.parse(" ! great")); + CHECK (not recurse.parse(" ! great")); CHECK (recurse.parse("great ! great actor").getResult() == 2); CHECK (recurse.parse("great ! great ! actor").getResult() == 2); + + + //_____________________________________________ + // Build a recursive numeric expression syntax... + auto num = accept("\\d+") .bindMatch().bind([](auto num){ return std::stod(num); }); + auto sqrt = accept("√").seq(num) .bind([](auto seq){ return std::sqrt(get<1>(seq)); }); + + CHECK (sqrt.parse(" √x ").getResult() == 0 ); + CHECK (sqrt.parse(" √2 ").getResult() == "1.4142136"_expect); + + // E ::= T [ + E ] + // T ::= F [ / F ] + // F ::= ( E ) | V + // V ::= num | √ num + auto expr = expectResult(); + + auto valu = accept(num).alt(sqrt) .bind([](auto alt){ return alt.getAny(); }); + auto fact = accept_bracket(expr).alt(valu) .bind([](auto alt){ return alt.getAny(); }); + auto term = accept(fact).opt(accept("/") .seq(fact)) .bind([](auto seq){ auto [f1,f2] = seq; return f1 / (f2? get<1>(*f2) : 1.0); }); + expr = accept(term).opt(accept("\\+").seq(expr)) .bind([](auto exp){ auto [s1,s2] = exp; return s1 + (s2? get<1>(*s2) : 0.0); }); + + CHECK (expr.canInvoke()); + CHECK (not expr.hasResult()); + + expr.parse(" 42 forever"); + CHECK (expr.success()); + CHECK (expr.getResult() == 42 ); + + expr.parse(" 42 + 13 =?"); + CHECK (expr.success()); + CHECK (expr.getResult() == 55 ); + + expr.parse(" 1 + 4/3 "); + CHECK (expr.success()); + CHECK (expr.getResult() == "2.3333333"_expect); + + expr.parse("(2+2)/(2+1) + 4/2"); + CHECK (expr.success()); + CHECK (expr.getResult() == "3.3333333"_expect); + + expr.parse("(1 + √5) / 2 "); + CHECK (expr.success()); + CHECK (expr.getResult() == "1.618034"_expect); } }; diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index 69efc7a2d..4c5e55c05 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -57029,8 +57029,7 @@ nebenbei abgefallen

- - + @@ -57045,8 +57044,7 @@ das muß tatsächlich ein Postfix-Operator sein

- - +
@@ -57057,8 +57055,7 @@ Vorschlag: bind(FUN)

- - +
@@ -57068,8 +57065,7 @@ damit klar gesagt wird, wenn die Funktion nicht den bisherigen Result-Typ nimmt

- -
+ @@ -57103,8 +57099,7 @@ Standard-Variante: bindMatch(n)

- - + @@ -57116,8 +57111,7 @@ ...denn es steuert die Art der Dekoration

- - + @@ -57140,8 +57134,7 @@ analog wie die anderen Combinatoren und buildConnex()

- - +
@@ -57155,8 +57148,7 @@ nachdem ich die Model-Fälle wegdiskutiert habe ☺

- - +
@@ -57191,8 +57183,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??

- - +
@@ -57205,8 +57196,7 @@ Für eine reale Anwendung sollte man möglichst tief unten mappen, und bäuchte auch ein Konzept, um auf einen gemeinsamen Ergebnis-Typ zu kommen, möglicherweise dann doch so etwas wie einen AST. Und wenn man es dann doch wirklich bräuchte, kann man's immer noch nachrüsten

- -
+
@@ -57235,8 +57225,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)

- - +
@@ -57261,8 +57250,7 @@ ganz banal: habe eval.consumed nicht weitergegeben

- - + @@ -57270,8 +57258,7 @@ d.h. jede sub-expression setzt wieder am Anfang auf

- -
+
@@ -57282,8 +57269,8 @@
- - + + @@ -57293,11 +57280,17 @@ - + - - - + + + + + + + + + @@ -57305,7 +57298,7 @@ - + @@ -57318,21 +57311,17 @@ ....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.

- -
+
@@ -57341,41 +57330,35 @@ - - - +

der Lambdas wegen

- -
+
- - - +

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

- -
+
- +
- + @@ -57401,34 +57384,28 @@ - + - - - +

und später an diese zuweisen

- -
+ - - - +

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

- -
+
@@ -57440,16 +57417,13 @@ - - - +

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

- -
+ @@ -57490,27 +57464,21 @@ - - - +

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)

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

- - +
@@ -57543,8 +57510,7 @@ Variante-1 ist »filosofisch« und praktsich attraktiv

- - + @@ -57552,8 +57518,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

- -
+
@@ -57563,8 +57528,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

- -
+
@@ -57579,8 +57543,7 @@ da sie ohnehin das erlaubt was man machen sollte, aber Fehl-Verwendungen unterbindet

- - +
@@ -57590,8 +57553,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

- -
+
@@ -57608,8 +57570,7 @@ ...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

- - +
@@ -57688,8 +57649,7 @@ da viele Builder-Funktionen in ein neues Syntax-Objekt schieben

- - +
@@ -57708,14 +57668,16 @@
- + + + - + @@ -57753,8 +57715,7 @@ ausdefinierte Syntax per Value (!) nehmen

- - +
@@ -57770,8 +57731,7 @@ ...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...

- - +
@@ -57787,9 +57747,78 @@
- + - + + + + + + + +

+ E ::= T [ + E ] +

+

+ T ::= F [ / F ] +

+

+ F ::= ( E ) | V +

+

+ V ::= num   |  num +

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

+ Spezialfall hier: homogenes Model +

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

+ ....denn nur so bekommt man den Aufwand in den Griff; +

+

+ ich demonstriere auch die (beabsichtigte) Anordnung im Quelltext, indem die bindings in eine Spalte rechts geschrieben werden; jede, wirklich jede Syntax-Klausel sollte den beabsichtigten Ergebnistyp haben (hier double), sonst läuft dieses Schema aus dem Ruder +

+ + +
+
+
+ + + + + + + +
@@ -57855,9 +57884,8 @@ - + -