lumiera_/src/lib/diff/gen-node.hpp
Ichthyostega bad4827b34 Upgrade: Literal can be constexpr
Only minor rearrangements necessary to make that possible with C++20
And while at this change (which requires a full rebuild of Lumiera)

- simplify the defined comparison operators, as C++20 can infer most variations
- also mark various usages of `const char*` either as Literal or CStr

Remark: regarding copyright, up to now this is entirely my work,
        with two major creation steps in 2008 (conception) and
        in 2017 (introduction of a symbol table)
2025-07-02 22:18:39 +02:00

984 lines
29 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
GEN-NODE.hpp - generic node element for tree like data representation
Copyright (C)
2015, Hermann Vosseler <Ichthyostega@web.de>
  **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 gen-node.hpp
** Generic building block for tree shaped (meta)data structures.
** A representation built from GenNode elements is intended to support
** (limited) introspection of data structures and exchange of mutations
** in the form of [diff messages](\ref diff-language.hpp).
**
** Despite of the name, GenNode is \em not meant to be an universal
** data representation; rather it is limited to embody a fixed hard
** wired set of data types, able to stand-in for attributes
** and sub scope contents of the lumiera high-level data model.
**
** # Anatomy of a GenNode
**
** GenNode is a polymorphic value with well defined identity and type.
** Each element is conceived to be »unique within context« -- as defined
** by the immediately visible scope within a tree like structure.
** Beyond this identity metadata, each GenNode carries a DataCap, which
** is an inline container and attachment point for payload data. Simple
** attribute values can be carried alongside, while more complex types
** or entities bound to a reference and registration system (e.g. Placement)
** will be referred by a suitable reference representation (PlacementID).
** The DataCap is what creates the polymorphic nature, where the common
** interface is mostly limited to managemental tasks (copying of values,
** external representation).
**
** To represent object-like structures and for building trees, a special
** kind of data type is placed into the DataCap. This type, Record<GenNode>
** is recursive and has the ability to hold both a a set of attributes
** addressable by-name and an (ordered) collection of elements treated
** as children within the scope of the given record.
**
** ## The GenNode ID
**
** Each GenNode holds an ID tag, allowing to establish _identical_ and _distinct_
** elements within a scope. This ID is based on lib::idi::BareEntryID, thereby
** providing a human readable symbolic part, and a hash value. By default, these
** GenNode IDs are fabricated such as to hold a non-reproducible, random hash
** value -- however, there are construction flavours allowing to pass in an
** pre-existing distinct Entry-ID.
**
**
** # Requirements
**
** GenNode elements are to be used in the diff detection and implementation.
** This implies some requirements for the (opaque) elements used in diff:
** - they need to support the notion of equality
** - we need to derive a key type for usage in index tables
** - this implies the necessity to support std::less comparisons for tree-maps
** - and the necessity to support hash code generation for unordered (hash)maps
** - moreover, the elements need to be values, able to be copied and handled at will
** - it will be beneficial for these values to support move semantics explicitly
** - in addition, the tree diffing suggests a mechanism to re-gain the fully
** typed context, either based on some kind of embedded type tag, or
** alternatively by visitation and matching
** - finally, the handling of changes prompts us to support installation
** of a specifically typed _change handling closure_.
**
** ## monadic nature?
**
** As suggested by the usage for representation of tree shaped data, we acknowledge
** that GenNode could be a Monad. We support the basic operation \em construction,
** and the operation \em flatMap would be trivial to add. To fit in with this generic
** processing pattern, the one element flavours of GenNode are considered the special case,
** while the collective flavours form the base case -- every GenNode can be iterated.
** The \em construction requirement suggests that GenNode may be created readily, just
** by wrapping any given and suitable element, thereby picking up the element's type.
**
** But the purpose and goal of the monadic approach is not clear yet (5/2015).
** To begin with, for the task of diff detection and application, it is sufficient
** to get the children as traversable collection and to offer a depth-first expansion.
**
** @see GenNode_test
** @see diff-list-generation-test.cpp
** @see DiffDetector
**
*/
#ifndef LIB_DIFF_GEN_NODE_H
#define LIB_DIFF_GEN_NODE_H
#include "lib/error.hpp"
#include "lib/idi/entry-id.hpp"
#include "lib/time/timevalue.hpp"
#include "lib/diff/record.hpp"
#include "lib/variant.hpp"
#include "lib/util.hpp"
#include <optional>
#include <utility>
#include <string>
#include <deque>
namespace lib {
namespace diff{
namespace error = lumiera::error;
struct GenNode;
struct Ref;
/** Define actual data storage and access types used */
template<>
struct RecordSetup<GenNode>
{
using Storage = std::vector<GenNode>;
using ElmIter = typename Storage::const_iterator;
/** using const reference data access
* relevant for handling large subtrees */
using Access = GenNode const&;
};
using Rec = Record<GenNode>;
using RecRef = RecordRef<GenNode>;
using MakeRec = Rec::Mutator;
using DataValues = meta::Types<int
,int64_t
,short
,char
,bool
,double
,string
,time::Time
,time::Offset
,time::Duration
,time::TimeSpan
,hash::LuidH
,RecRef
,Rec
>;
class DataCap
: public Variant<DataValues>
{
public:
template<typename X>
DataCap(X&& x)
: Variant<DataValues>(std::forward<X>(x))
{ }
////////////////////////TICKET #963 Forwarding shadows copy operations -- generic solution??
DataCap(DataCap const&) =default;
DataCap(DataCap&&) =default;
DataCap(DataCap& o)
: DataCap((DataCap const&)o)
{ }
DataCap& operator= (DataCap const&) =default;
DataCap& operator= (DataCap&&) =default;
bool matchData (DataCap const&) const;
bool matchNum (int64_t) const;
bool matchTxt (string const&) const;
bool matchTime (time::TimeValue) const;
bool matchBool (bool) const;
bool matchDbl (double) const;
bool matchLuid (hash::LuidH) const;
bool matchRec (RecRef const&) const;
bool matchRec (Rec const&) const;
struct Locator;
Locator expand() const;
operator string() const;
template<typename X>
X& get();
template<typename X>
X const& get() const;
/** visit _children_ of a nested `Record<GenNode>` */
Rec::scopeIter
childIter() const
{
const Rec* rec = unConst(this)->maybeGet<Rec>();
if (!rec)
return Rec::scopeIter();
else
return rec->scope();
}
/** determine if payload constitutes a nested scope ("object") */
bool isNested() const;
/** peek into the type field of a nested `Record<GenNode>` */
string recordType() const;
/** peek into the attributes of a nested Record */
template<typename X>
std::optional<X>
retrieveAttribute (string key) const;
bool hasAttribute (string key) const;
private:
Rec* maybeAccessNestedRec();
};
/** generic data element node within a tree */
struct GenNode
{
class ID
: public idi::BareEntryID
{
friend struct GenNode;
template<typename X>
ID (X*, string const& symbolicID)
: idi::BareEntryID (symbolicID,
idi::getTypeHash<X>())
{ }
ID (idi::BareEntryID&& rawD)
: idi::BareEntryID{move (rawD)}
{ }
public:
explicit
ID (GenNode const& node)
: ID(node.idi)
{ }
// standard copy operations acceptable
operator string() const
{
return "ID(\""+getSym()+"\")";
}
};
//------GenNode Data fields---
ID idi;
DataCap data;
template<typename X>
GenNode(X&& val)
: idi(&val, buildChildID<X>())
, data(std::forward<X>(val))
{ }
template<typename X>
GenNode(string const& symbolicID, X&& val)
: idi(&val, symbolicID)
, data(std::forward<X>(val))
{ }
GenNode(string const& symbolicID, CStr text)
: GenNode(symbolicID, string(text))
{ }
GenNode(CStr text)
: GenNode(string(text))
{ }
////////////////////////TICKET #963 Forwarding shadows copy operations -- generic solution??
GenNode(GenNode const&) =default;
GenNode(GenNode&&) =default;
GenNode(GenNode& o) : GenNode((GenNode const&)o) { }
GenNode(Ref const& r);
GenNode(Ref & r);
GenNode(Ref && r);
/** copy assignment
* @remarks we need to define our own version here for sake of sanity.
* The reason is that we use inline storage (embedded within lib::Variant)
* and that we deliberately _erase_ the actual type of data stored inline.
* Because we still do want copy assignment, in case the payload data
* supports this, we use a "virtual copy operator", where in the end
* the storage buffer within lib::Variant has to decide if assignment
* is possible. Only data with the same type may be assigned and we
* prevent change of the (implicit) data type through assignment.
* This check might throw, and for that reason we're better off
* to perform the _data assignment_ first. The probability for
* EntryID assignment to fail is low (but it may happen!).
* @note the use of inline storage turns swapping of data
* into an expensive operation, involving a temporary.
* This rules out the copy-and-swap idiom.
*/
GenNode&
operator= (GenNode const& o)
{
if (&o != this)
{
data = o.data;
idi = o.idi;
}
return *this;
}
GenNode&
operator= (GenNode&& o)
{
ASSERT (&o != this);
data = std::forward<DataCap>(o.data);
idi = std::forward<ID>(o.idi);
return *this;
}
//note: NOT defining a swap operation, because swapping inline storage is pointless!
/** @internal diagnostics helper */
operator string() const
{
return "GenNode-"+string(idi)+"-"+string(data);
}
bool
isNamed() const
{
return not util::startsWith (idi.getSym(), "_CHILD_");
}
bool
isTypeID() const
{
return "type" == idi.getSym();
}
template<typename X>
bool contains (X const& elm) const;
bool matches (GenNode const& o) const { return this->matches(o.idi); } ///< @note _not_ comparing payload data. Use equality for that…
bool matches (ID const& id) const { return idi == id; }
bool matches (int number) const { return data.matchNum(number);}
bool matches (int64_t number) const { return data.matchNum(number);}
bool matches (short number) const { return data.matchNum(number);}
bool matches (char number) const { return data.matchNum(number);}
bool matches (double number) const { return data.matchDbl(number);}
bool matches (string text) const { return data.matchTxt(text);}
bool matches (const char* text) const { return data.matchTxt(text);}
bool matches (time::TimeValue t) const { return data.matchTime(t); }
bool matches (bool b) const { return data.matchBool(b); }
bool matches (hash::LuidH h) const { return data.matchLuid(h); }
bool matches (RecRef const& ref) const { return data.matchRec(ref); }
bool matches (Rec const& rec) const { return data.matchRec(rec); }
class ScopeExplorer;
struct ScopeExplorerIterator;
using iterator = ScopeExplorerIterator;
iterator begin() ;
iterator begin() const;
iterator end() ;
iterator end() const;
using ChildDataIter = TransformIter<Rec::scopeIter, DataCap const&>;
/** visit the _data_ of nested child elements
* @return an iterator over the DataCap elements of all children,
* in case this GenNode actually holds a Record.
* Otherwise an empty iterator.
* @note this iterator visits _only_ the children, which are
* by definition unnamed. It does _not_ visit attributes.
*/
friend ChildDataIter
childData (GenNode const& n)
{
return ChildDataIter{ n.data.childIter()
, [](GenNode const& child) ->DataCap const&
{
return child.data;
}
};
}
friend ChildDataIter
childData (Rec::scopeIter&& scopeIter)
{
return ChildDataIter{ std::forward<Rec::scopeIter>(scopeIter)
, [](GenNode const& child) ->DataCap const&
{
return child.data;
}
};
}
friend string
name (GenNode const& node)
{
return node.idi.getSym();
}
friend bool
operator== (GenNode const& n1, GenNode const& n2)
{
return n1.idi == n2.idi
and n1.data.matchData(n2.data);
}
friend bool
operator!= (GenNode const& n1, GenNode const& n2)
{
return not (n1 == n2);
}
/**
* allow for storage in ordered containers, ordering
* based on the human-readable ID within the GenNode.
* @warning this constitutes a _weaker equivalence_ than
* given by the equality comparison (`operator==`),
* since GenNode::ID is an EntryID, which also includes
* the type parameter into the identity (hash). This means,
* two GenNodes with different real payload type but same
* ID symbol will not be equal, but be deemed equivalent
* by this IDComparator. This can be dangerous when building
* a set or map based on this comparator.
*/
struct IDComparator
{
bool
operator() (GenNode const& left, GenNode const& right) const
{
return left.idi.getSym() < right.idi.getSym();
}
};
/** fabricate a GenNode with the literally given ID */
template<typename X>
static GenNode
asAttribute (idi::BareEntryID && rawID, X&& payload)
{
return GenNode{ID{move (rawID)}, DataCap{forward<X> (payload)}};
}
/** mismatch tolerant convenience shortcut to peek
* into the attributes of a nested Record */
template<typename X>
std::optional<X>
retrieveAttribute (string key) const;
bool hasAttribute (string key) const;
bool isNested() const;
bool hasChildren() const;
Rec::scopeIter getChildren() const;
protected:
/** @internal for dedicated builder subclasses */
GenNode (ID&& id, DataCap&& d)
: idi(std::move(id))
, data(std::move(d))
{ }
template<typename X>
static GenNode::ID
fabricateRefID (string const& symbolicID)
{
X* typeID(0);
return ID(typeID, symbolicID);
}
private:
template<typename X>
static string
buildChildID()
{
return "_CHILD_" + idi::generateSymbolicID<X>();
}
};
/** compact textual representation of a GenNode tree */
string renderCompact (GenNode const&);
string renderCompact (RecRef const&);
string renderCompact (Rec const&);
/**
* metafunction to detect types able to be wrapped into a GenNode.
* Only a limited and fixed set of types may be placed within a GenNode,
* as defined through the typelist `lib::diff::DataValues`. This metafunction
* allows to enable or disable specialisations and definitions based on the
* fact if a type in question can live within a GenNode.
*/
template<typename ELM>
struct can_wrap_in_GenNode
{
using Yes = lib::meta::Yes_t;
using No = lib::meta::No_t;
template<class X>
static Yes check(typename variant::CanBuildFrom<X, DataValues>::Type*);
template<class X>
static No check(...);
public:
static const bool value = (sizeof(Yes)==sizeof(check<ELM>(0)));
};
/* === iteration / recursive expansion === */
/**
* @internal Helper to refer to any element position,
* irrespective if on top level or within a nested scope
* @remarks typically used within lib::IterStateWrapper
* @see DataCap#expand()
*/
struct DataCap::Locator
{
const GenNode* node_;
Rec::iterator scope_;
Locator()
: node_(nullptr)
{ }
Locator(GenNode const& n)
: node_(&n)
{ }
Locator(Rec const& r)
: node_(nullptr)
, scope_(r.begin())
{ }
const GenNode *
get() const
{
return node_? node_
: scope_? scope_.operator->()
: nullptr;
}
/* === Iteration control API for IterStateWrapper == */
bool
checkPoint() const
{
return this->get();
}
GenNode const&
yield() const
{
return *get();
}
void
iterNext()
{
if (node_)
node_ = nullptr;
else
++scope_;
}
};
/**
* Building block for monad-like depth-first expansion of a GenNode.
* When used within lib::IterStateWrapper, the result is an Iterator
* to visit the contents of a GenNode tree recursively depth-fist.
*/
class GenNode::ScopeExplorer
{
using ScopeIter = IterStateWrapper<DataCap::Locator>;
std::deque<ScopeIter> scopes_;
public:
ScopeExplorer() { }
ScopeExplorer(GenNode const& n)
{
scopes_.emplace_back(n);
}
size_t
depth() const
{
return scopes_.size();
}
/* === Iteration control API for IterStateWrapper == */
bool
checkPoint() const
{
return not scopes_.empty()
and bool(scopes_.back());
}
GenNode const&
yield() const
{
return *(scopes_.back());
}
void
iterNext()
{
ScopeIter& current = scopes_.back();
scopes_.emplace_back (current->data.expand());
++current;
while (not scopes_.empty() and not scopes_.back())
scopes_.pop_back();
}
friend bool
operator== (ScopeExplorer const& s1, ScopeExplorer const& s2)
{
return not s1.scopes_.empty()
and not s2.scopes_.empty()
and s1.scopes_.size() == s2.scopes_.size()
and s1.yield() == s2.yield();
}
};
/** @internal Core operation to expand nested scopes recursively */
inline DataCap::Locator
DataCap::expand() const
{
Rec* val = unConst(this)->maybeGet<Rec>();
if (!val)
return Locator();
else
return Locator(*val);
}
struct GenNode::ScopeExplorerIterator
: IterStateWrapper<ScopeExplorer>
{
using IterStateWrapper::IterStateWrapper;
size_t level() const { return unConst(this)->stateCore().depth(); }
};
inline GenNode::iterator GenNode::begin() { return iterator(*this); }
inline GenNode::iterator GenNode::begin() const { return iterator(*this); }
inline GenNode::iterator GenNode::end() { return iterator(); }
inline GenNode::iterator GenNode::end() const { return iterator(); }
template<typename X>
inline bool
GenNode::contains (X const& elm) const
{
for (auto & n : *this)
if (n.matches(elm))
return true;
return false;
}
/* === References : special treatment on element access === */
template<typename X>
inline X&
DataCap::get()
{
return Variant<DataValues>::get<X>();
}
template<typename X>
inline X const&
DataCap::get() const
{
return Variant<DataValues>::get<X>();
}
/** especially when accessing for a Record,
* a payload of type \c RecordRef<Record<GenNode>> (aka RecRef)
* will be automatically dereferenced. Effectively this allows
* a GenNode with a RecRef payload to "stand in" for a node holding
* a full Record inline. And it allows the construction of a special
* \link #Ref Ref-GenNode \endlink, which even shares the \em identity
* (the ID) of the referenced record-GenNode.
* @note effectively this opens an indirect loophole to const correctness,
* since it is possible explicitly to retrieve the RecRef from a
* const GenNode and then to access the referred-to Record
* without const. In case this turns out to be problematic,
* we'd have to alter the semantics of RecRef
*/
template<>
inline Rec&
DataCap::get()
{
Rec* rec = maybeGet<Rec>();
if (rec) return *rec;
return Variant<DataValues>::get<RecRef>();
}
template<>
inline Rec const&
DataCap::get() const
{
Rec* rec = unConst(this)->maybeGet<Rec>();
if (rec) return *rec;
return Variant<DataValues>::get<RecRef>();
}
/** @internal helper to possibly peek into a nested record */
inline Rec*
DataCap::maybeAccessNestedRec()
{
Rec* nested = maybeGet<Rec>();
if (!nested)
{ // 2nd try: maybe we hold a reference?
RecRef* ref = maybeGet<RecRef>();
if (ref and not ref->empty())
nested = ref->get();
}
return nested;
}
/**
* @return either the contents of a nested record's type field
* or the util::BOTTOM_INDICATOR, when not a record.
* @remarks this function never raises an error, even if the element
* in fact doesn't constitute a nested scope. Effectively this
* allows to "peek" into the contents to some degree.
*/
inline string
DataCap::recordType() const
{
Rec* nested = unConst(this)->maybeAccessNestedRec();
return nested? nested->getType()
: util::BOTTOM_INDICATOR;
}
inline bool
DataCap::isNested() const
{
return nullptr != unConst(this)->maybeAccessNestedRec();
}
template<typename X>
inline std::optional<X>
DataCap::retrieveAttribute (string key) const
{
static_assert (not std::is_reference_v<X>
,"optional access only possible by value");
Rec* nested = unConst(this)->maybeAccessNestedRec();
if (nested and nested->hasAttribute (key))
{
DataCap const& nestedAttributeData = nested->get(key).data;
X* payload = unConst(nestedAttributeData).maybeGet<X>();
if (payload) return *payload; // Note: payload copied into optional
}
return std::nullopt;
}
inline bool
DataCap::hasAttribute (string key) const
{
Rec* nested = unConst(this)->maybeAccessNestedRec();
return nested and nested->hasAttribute (key);
}
template<typename X>
inline std::optional<X>
GenNode::retrieveAttribute (string key) const
{
return data.retrieveAttribute<X> (key);
}
inline bool
GenNode::hasAttribute (string key) const
{
return data.hasAttribute (key);
}
inline bool
GenNode::isNested() const
{
return data.isNested();
}
inline bool
GenNode::hasChildren() const
{
return not isnil (data.childIter());
}
inline Rec::scopeIter
GenNode::getChildren() const
{
return data.childIter();
}
/**
* Constructor for a specially crafted 'ref GenNode'.
* The identity record of the generated object will be prepared
* such as to be identical to a regular GenNode with Record payload.
* @note slicing in usage is intentional
*/
struct Ref
: GenNode
{
/** create an empty ID stand-in.
* @note the purpose is to create a symbolic reference by name
*/
explicit
Ref(string const& symbolicID)
: GenNode(fabricateRefID<Rec> (symbolicID)//note: seeds the type hash with Rec, not RecRef
, DataCap(RecRef())) // note: places NIL into the reference part
{ }
/** build reference to a Record, using the original ID
* @throw error::Logic when oNode does not hold a Record<GenNode>
*/
Ref(GenNode& oNode)
: GenNode(ID(oNode)
, DataCap(RecRef(oNode.data.get<Rec>())))
{ }
static const Ref I; ///< symbolic ID ref "_I_"
static const Ref NO; ///< symbolic ID ref "_NO_"
static const Ref END; ///< symbolic ID ref "_END_"
static const Ref THIS; ///< symbolic ID ref "_THIS_"
static const Ref CHILD; ///< symbolic ID ref "_CHILD_"
static const Ref ATTRIBS; ///< symbolic ID ref "_ATTRIBS_"
};
// slice down on copy construction...
inline GenNode::GenNode(Ref const& r) : idi(r.idi), data(r.data) { }
inline GenNode::GenNode(Ref & r) : idi(r.idi), data(r.data) { }
inline GenNode::GenNode(Ref && r) : idi(std::move(r.idi)),
data(std::move(r.data)) { }
/* === Specialisation to add fluent GenNode builder API to Record<GenNode> === */
template<>
inline GenNode
MakeRec::genNode()
{
return GenNode{std::move(record_)};
}
template<>
inline GenNode
MakeRec::genNode (idi::BareEntryID rawID)
{
return GenNode::asAttribute (std::move(rawID), std::move(record_));
}
template<>
inline GenNode
MakeRec::genNode (string const& symbolicID)
{
return GenNode{symbolicID, std::move(record_)};
}
/* === Extension point to apply a tree-diff === */
/** implementation is provided by the "diff framework"
* @see tree-mutator-gen-node-binding.hpp
* @see tree-diff.cpp (implementation)
*/
template<>
void MakeRec::buildMutator (BufferHandle buff);
/* === Specialisation for handling of attributes in Record<GenNode> === */
template<>
inline bool
Rec::isAttribute (GenNode const& attrib)
{
return attrib.isNamed();
}
template<>
inline bool
Rec::isTypeID (GenNode const& attrib)
{
return attrib.isTypeID();
}
template<>
inline string
Rec::extractTypeID (GenNode const& v)
{
return isTypeID(v)? v.data.get<string>()
: Rec::TYPE_NIL;
}
template<>
inline string
Rec::extractKey (GenNode const& v)
{
return isAttribute(v)? v.idi.getSym()
: "";
}
template<>
inline GenNode const&
Rec::extractVal (GenNode const& v)
{
return v;
}
template<>
inline string
Rec::renderAttribute (GenNode const& a)
{
return a.idi.getSym() +" = "+ string(a.data);
}
template<>
template<typename X>
inline GenNode
Rec::buildAttribute (string const& key, X&& payload)
{
return GenNode{key, forward<X>(payload)};
}
}// namespace lib::diff
namespace variant {
using diff::Rec;
/**
* specialisation allowing to build a GenNode with a
* nested record from a Record::Mutator (aka `MakeRec`)
*/
template<typename TYPES>
struct CanBuildFrom<diff::MakeRec, Node<Rec, TYPES>>
: std::true_type
{
using Type = Rec;
};
}} // namespace lib::variant
#endif /*LIB_DIFF_GEN_NODE_H*/