From 3e743ff3b5aa2572ae456c052929136af3294e18 Mon Sep 17 00:00:00 2001 From: Ichthyostega Date: Sun, 19 Jan 2025 23:11:25 +0100 Subject: [PATCH] Library: explore design of a Sum-Type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit To represent the result-model for syntax alternatives, we need a C++ representation for a ''sum type,'' i.e. a type that can be one from a fixed set of alternatives. Obviously the implementation will rely on some kind of Union, or otherwise employ an opaque buffer and perform a forced cast. Moreover, to be actually usable, a branch-selector-ID must be captured and stored alongside, so that code processing the results can detect which branch of the syntax was chosen. There seem to be several possible avenues to build and structure an actual class template to provide this implementation model * a nested decorator-chain * using a recursive selector-function with a generic-λ ''all these look quite unattractive, unfortunately....'' --- src/lib/parse.hpp | 110 +++++++- tests/library/parse-test.cpp | 17 ++ wiki/thinkPad.ichthyo.mm | 512 +++++++++++++++++++++++++++++++++++ 3 files changed, 637 insertions(+), 2 deletions(-) diff --git a/src/lib/parse.hpp b/src/lib/parse.hpp index 86fb4b21f..cd7b524a8 100644 --- a/src/lib/parse.hpp +++ b/src/lib/parse.hpp @@ -34,6 +34,9 @@ #include #include +#include +#include +#include namespace util { namespace parse { @@ -46,12 +49,115 @@ namespace util { using lib::meta::NullType; using std::decay_t; using std::tuple; + using std::array; using StrView = std::string_view; - + template + struct _MaxBufSiz; + template<> + struct _MaxBufSiz<> + { + static constexpr size_t siz = 0; + }; + template + struct _MaxBufSiz + { + static constexpr size_t siz = std::max (sizeof(T) + ,_MaxBufSiz::siz); + }; - /** + /** + * Data storage base for sum type with selector + */ + template + struct OpaqueSumType + { + size_t case_{0}; + + alignas(int64_t) + std::byte buffer_[SIZ]; + + template + T& + emplace (INITS&&...inits) + { + case_ = slot; + return * new(&buffer_) T(forward (inits)...); + } + + }; + + template + class SumType + : private OpaqueSumType<_MaxBufSiz::siz> + { + public: + static constexpr size_t TOP = sizeof...(TYPES); + static constexpr size_t SIZ = _MaxBufSiz::siz; + using _Opaque = OpaqueSumType; + + template> + SumType (INITS&& ...inits) + { + _Opaque::template emplace (forward (inits)...); + } + + template + TX& + access () + { + return * std::launder (reinterpret_cast (& _Opaque::buffer_[0])); + } + + size_t + selected() const + { + return _Opaque::case_; + } + + template + using SlotType = std::tuple_element_t>; + + template + SlotType& + get() + { + return access>(); + } + + template + void + select (size_t slot, FUN&& fun) + { + REQUIRE (slot <= sizeof...(TS)); + if constexpr (sizeof...(TS)) + if (0 < slot) + select (slot-1, forward(fun)); + fun (access()); + } + + template + void + destroyIt (TX& it) + { + it.~TX(); + } + + void + destroy (size_t slot) + { + select (slot, [&](auto& it){ destroyIt(it); }); + } + + ~SumType() + { + destroy (_Opaque::case_); + } + }; + + + /** */ template struct Eval diff --git a/tests/library/parse-test.cpp b/tests/library/parse-test.cpp index 11eea386d..4750e4f6f 100644 --- a/tests/library/parse-test.cpp +++ b/tests/library/parse-test.cpp @@ -77,6 +77,7 @@ namespace test { simpleBlah(); acceptTerminal(); acceptSequential(); + acceptAlternatives(); } @@ -209,6 +210,22 @@ namespace test { CHECK (get<1>(seqModel2).str() == "world"_expect); CHECK (get<2>(seqModel2).str() == "trade"_expect); } + + + /** @test TODO define alternative syntax structures to match by parse. */ + void + acceptAlternatives() + { + using Sum = SumType; +SHOW_EXPR(sizeof(Sum)); + Sum sumt{42}; +SHOW_EXPR(sumt.selected()); +SHOW_EXPR(sumt.SIZ); +SHOW_EXPR(sumt.TOP); +SHOW_TYPE(Sum::_Opaque) +SHOW_EXPR(sumt.get<1>()); +SHOW_EXPR(sumt.get<0>()); + } }; LAUNCHER (Parse_test, "unit common"); diff --git a/wiki/thinkPad.ichthyo.mm b/wiki/thinkPad.ichthyo.mm index e94813d59..38422a0dc 100644 --- a/wiki/thinkPad.ichthyo.mm +++ b/wiki/thinkPad.ichthyo.mm @@ -56028,6 +56028,439 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ bei der Sequenz wurde die gleiche Annahme gemacht, und daß der Optimiser das schon wuppen wird +

+ + +
+
+ + + + + + +

+ warum? weil eine Folge zunehmend variantenreicherer Summen-Typen entsteht, und die Puffergröße ist das Maximum des benötigten Platzes +

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

+ das ist ähnlich zu einer persistenten Datenstruktur: man fragt den jeweiligen Vorgänger-Datentyp mit dem dekrementierten Selektor an; ist der Selektor 0, wird der dort fest gecodete Typ-Zugriff verwendet +

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

+ das heißt, die Operationen führt stets derjenige partielle Summen-Typ aus, bei dem der aktive Zweig auf der Top-Position (am Anfang) steht; der aktuelle Selektor-Wert legt also das »Stockwerk« fest, auf dem gearbeitet wird +

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

+ ...weil der Compiler ja nichts über den Selektor-Wert weiß, muß er in jedem Fall die gesamte rekursive Kette instantiieren; das Ende der Typ-Sequenz stellt also eine zweite Abbruchbedinung der Rekursion bereit, und diese muß so in die Kontrollstruktur eingebunden sein, daß die rekursiven Instantiierungen wirklich aufhören +

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

+ Fazit: mit Einschränkumg umsetzbar... +

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

+ Und zwar im Bezug auf allgemeine Handwerksregeln... +

+
    +
  • + man könnte ohne Basisklasse auskommen +
  • +
  • + der opaque-Bufer wäre als Implementierungsdetail direkt im private-Scope +
  • +
  • + es wird nur eine einzige Klasse (pro Typ-Signatur) tatsächlich instantiiert +
  • +
+ + +
+
+ + + + + + +

+ Das fängt schon damit an, daß man doch noch getemplatete Funktionen für die Elementar-Operationen schreiben muß (zumindest in C++17). Dazu kommt dann diese doch einigermaßen trickreiche generische select-Operation +

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

+ konkret stellt das zufälligerweise kein Problem dar +

+ + +
+ + + + + + +

+ das erscheint zunächst wie »glückliche Umstände«; +

+

+ bei genauerer Überlegung wird aber klar, daß ich diese Einsicht bereits intuitiv angewendet hatte, als ich von Vornherein auch ein Selektor-Feld mit disponierte; es ist nämlich so, daß wir uns vermöge des Selektor-Feldes genau diese explizite Info zur Compile-Zeit beschaffen können, indem der aufrufende Code ein switch-case auf den Selector macht. +

+ + +
+
+ + + + + + +

+ das im Prototyp entworfene select(λ) ist ein Variant-Visitor +

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

+ template<typename TX, typename...TS, class FUN> +

+

+ void +

+

+ select (size_t slot, FUN&& fun) +

+

+   { +

+

+     REQUIRE (slot <= sizeof...(TS)); +

+

+     if constexpr (sizeof...(TS)) +

+

+       if (0 < slot) +

+

+         select<TS..> (slot-1, forward<FUN>(fun)); +

+

+     fun (access<TX>()); +

+

+   } +

+ + +
+ + + + + + + +

+ Das funktioniert nur wenn man einige subtile Details richtig hinbekommt.... +

+
    +
  • + die Abbruchbedingung muß für den Compiler sichtbar sein, daher als constexpr-if zu formulieren +
  • +
  • + der Funktor oder das Lambda muß wirklich per offenem Template-Argument übergeben werden, weil es tatsächlich selber ein Template sein muß (generische Lambda bieten hierfür eine abgekürzte Schreibweise) +
  • +
  • + der Zugriff ist trotzdem komplett ungeschützt +
  • +
  • + man muß also anderweitig dafür sorgen, daß die slot-# tatsächlich korrekt belegt ist +
  • +
+ + +
+ +
+
+ + + +
+ + + + + + + + + +

+ wenn man ohnehin eine bestimmte Syntax parsen möchte, ist es naheliegend und natürlich, auf den vorliegenden konkreten Zweig zu prüfen und zu verzweigen; in dieser Form erscheint das eine vertretbare und nicht weiter gefährliche Herangehensweise. Dieser Ansatz kann allerdings durchaus in einen schwer wartbaren Zustand abgleiten, wenn die Syntax komplex und heterogen strukturiert ist; dann muß man die Varianten mehr oder weniger im Kopf haben. Für dieses Problem sehe ich keine allgemeingültige Lösung... +

+ + +
+
+ + + + + + +

+ Das ist eine schöne neue Möglichkeit, die sich durch die generischen Lambdas auftut; durchaus möglich, daß in manchen Fällen eine solche Formulierung eine drastische Vereinfachung darstellt, beispielsweise wenn alle Zweige jeweils einen RegExp-Match mit äquivalent angeordneten Capture-Groups beinhalten; die Einzelfälle würden damit sozusagen transparent verschmolzen. +

+ + +
+
+
+
+
+
+
+
@@ -69239,6 +69672,10 @@
+ + + + @@ -152710,6 +153147,81 @@ unsigned int ThreadIdAsInt = *static_cast<unsigned int*>(static_cast<vo + + + + + + + + +

+ Design-Diskussion »Visitor« +

+ +
+ + + + + +

+ Die Überlegungen zum Visitor und double-Dispatch tauchten bereits in meinen allerersten Design-Studien zu Lumiera auf. Zunächst war mir nur undeutlich ein Zusammenhang klar, warum ich in diese Richtung strebte, erst im Lauf der Jahre enthüllte sich ein größerer Zusammenhang: es ist die Suche nach einer Struktur, die weder von Anbeginn an feste Setzungen macht, aber auch nicht bloß vom Einzelfall getrieben ist. Wie kann man eine gestaltete Struktur bauen und halten, ohne mit einer gewaltsamen Setzung zu beginnen, die im Weitergehen doch zwansläufig zuschanden werden muß? Wie verhindert man, sich in eine Sackgasse zu bringen, indem man sich einfach am naheliegend gegebenen entlanghangelt? Wie vermeidet man Ideenflucht und das „we'll figure it out“-Syndrom? +

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

+ wie kann man einen Compiler anlegen, +

+

+ ohne bereits zu wissen, was compiliert wird? +

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

+ Problem: »Event«-getriebene Struktur +

+ +
+ + + + + + +

+ multiple »Domain Ontologies«? +

+ +
+
+