lumiera_documentation/src/lib/path-array.hpp

586 lines
18 KiB
C++
Raw Normal View History

/*
PATH-ARRAY.hpp - sequence of path-like component-IDs in fixed storage
Copyright: clarify and simplify the file headers * Lumiera source code always was copyrighted by individual contributors * there is no entity "Lumiera.org" which holds any copyrights * Lumiera source code is provided under the GPL Version 2+ == Explanations == Lumiera as a whole is distributed under Copyleft, GNU General Public License Version 2 or above. For this to become legally effective, the ''File COPYING in the root directory is sufficient.'' The licensing header in each file is not strictly necessary, yet considered good practice; attaching a licence notice increases the likeliness that this information is retained in case someone extracts individual code files. However, it is not by the presence of some text, that legally binding licensing terms become effective; rather the fact matters that a given piece of code was provably copyrighted and published under a license. Even reformatting the code, renaming some variables or deleting parts of the code will not alter this legal situation, but rather creates a derivative work, which is likewise covered by the GPL! The most relevant information in the file header is the notice regarding the time of the first individual copyright claim. By virtue of this initial copyright, the first author is entitled to choose the terms of licensing. All further modifications are permitted and covered by the License. The specific wording or format of the copyright header is not legally relevant, as long as the intention to publish under the GPL remains clear. The extended wording was based on a recommendation by the FSF. It can be shortened, because the full terms of the license are provided alongside the distribution, in the file COPYING.
2024-11-17 23:42:55 +01:00
Copyright (C)
2017, Hermann Vosseler <Ichthyostega@web.de>
Copyright: clarify and simplify the file headers * Lumiera source code always was copyrighted by individual contributors * there is no entity "Lumiera.org" which holds any copyrights * Lumiera source code is provided under the GPL Version 2+ == Explanations == Lumiera as a whole is distributed under Copyleft, GNU General Public License Version 2 or above. For this to become legally effective, the ''File COPYING in the root directory is sufficient.'' The licensing header in each file is not strictly necessary, yet considered good practice; attaching a licence notice increases the likeliness that this information is retained in case someone extracts individual code files. However, it is not by the presence of some text, that legally binding licensing terms become effective; rather the fact matters that a given piece of code was provably copyrighted and published under a license. Even reformatting the code, renaming some variables or deleting parts of the code will not alter this legal situation, but rather creates a derivative work, which is likewise covered by the GPL! The most relevant information in the file header is the notice regarding the time of the first individual copyright claim. By virtue of this initial copyright, the first author is entitled to choose the terms of licensing. All further modifications are permitted and covered by the License. The specific wording or format of the copyright header is not legally relevant, as long as the intention to publish under the GPL remains clear. The extended wording was based on a recommendation by the FSF. It can be shortened, because the full terms of the license are provided alongside the distribution, in the file COPYING.
2024-11-17 23:42:55 +01:00
  **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 path-array.hpp
** Foundation abstraction to implement path-like component sequences.
** This library class can be used to build a path abstraction for data structure access
** or some similar topological coordinate system, like e.g. [UI coordinates](\ref ui-coord.hpp)
** A PathArray is an iterable sequence of literal component IDs, implemented as tuple of lib::Literal
** held in fixed inline storage with possible heap allocated (and thus unlimited) extension storage.
** It offers range checks, standard iteration and array-like indexed component access; as a whole
** it is copyable, while actual components are immutable after construction. PathArray can be
** bulk initialised from an explicit sequence of literals; it is normalised on construction
** to trim and fill interspersed missing elements. Iteration and equality comparison are
** built on top of the normalisation; iteration starts with the first nonempty element.
**
** @remark the choice of implementation layout is arbitrary and not based on evidence.
** A delegate structure with fixed inline storage looked like an interesting programming challenge.
** Using just a heap based array storage would have been viable likewise.
** @todo when UICoord is in widespread use, collect performance statistics and revisit this design.
**
** @see PathArray_test
** @see UICoord_test
** @see stage::interact::UICoord
** @see view-spec-dsl.hpp
*/
#ifndef LIB_PATH_ARRAY_H
#define LIB_PATH_ARRAY_H
#include "lib/error.hpp"
#include "lib/symbol.hpp"
#include "lib/iter-adapter.hpp"
#include "lib/meta/variadic-helper.hpp"
#include "lib/format-obj.hpp"
#include "lib/util.hpp"
#include <algorithm>
#include <utility>
#include <string>
#include <memory>
#include <array>
namespace lib {
namespace error = lumiera::error;
using std::string;
using std::forward;
using lib::Literal;
using util::unConst;
2017-10-01 01:30:53 +02:00
using util::isnil;
namespace con { // Implementation helper: flexible heap based extension storage....
/**
* Heap-allocated extension storage for an immutable sequence of literal strings.
* The size of the allocation is determined and fixed once, at construction time,
* derived from the number of initialisers. The first slot within the allocation
* stores this length. Extension can be _empty_ (default constructed),
* in which case no heap allocation is performed.
*/
class Extension
{
using PStorage = Literal*;
PStorage storage_;
static size_t&
size (PStorage& p)
{
REQUIRE (p);
return reinterpret_cast<size_t&> (p[0]);
}
/** allocate a copy.
* @note caller has to manage the allocated memory
* @warning call to Literal's ctor deliberately elided
*/
PStorage
newCopy() const
{
size_t siz = 1 + size (unConst(this)->storage_);
const char** alloc = new const char*[siz];
std::copy (storage_, storage_+siz, alloc);
return reinterpret_cast<PStorage> (alloc);
}
public:
~Extension()
{
if (storage_)
delete[] storage_;
}
Extension() noexcept
: storage_{nullptr}
{ }
template<typename...ELMS>
explicit
Extension (ELMS&& ...elms)
: storage_{new Literal[1 + sizeof...(ELMS)]} // proper alignment maintained here (TICKET #1204)
{
size(storage_) = sizeof...(ELMS);
new(storage_+1) Literal[sizeof...(ELMS)] {forward<ELMS>(elms)...};
}
Extension (Extension const& r)
: storage_{r.storage_? r.newCopy() : nullptr}
{ }
Extension (Extension&& rr) noexcept
: storage_{nullptr}
{
if (rr.storage_)
std::swap (storage_, rr.storage_);
}
Extension& operator= (Extension o)
{
std::swap (storage_, o.storage_);
return *this;
}
operator bool() const { return not empty(); }
bool empty() const { return not storage_;}
size_t
size() const
{
return storage_? size(unConst(this)->storage_)
: 0;
}
Literal const&
operator[] (size_t idx) const
{
REQUIRE (storage_ and idx < size());
return storage_[1+idx];
}
size_t
indexOf (Literal const* pos) const
{
REQUIRE (isValid (pos));
return pos - (storage_+1);
}
bool
isValid (Literal const* pos) const
{
return storage_
and storage_ < pos
and pos < storage_ + (1 + size (unConst(this)->storage_));
}
2017-10-01 01:30:53 +02:00
void
resizeTo (size_t cnt)
2017-10-01 01:30:53 +02:00
{
if (cnt > size())
{ // need more storage
if (storage_)
{ // copy content to new expanded storage
auto target = new const char* [cnt+1];
auto pos = std::copy (storage_, storage_+1+size(), target);
for ( ; pos < target+1+cnt; ++pos)
*pos = nullptr;
delete[] storage_;
storage_ = reinterpret_cast<PStorage> (target);
}
else // allocate and init empty
storage_ = new Literal [cnt+1];
size (storage_) = cnt;
return;
}
if (not storage_) return;
if (cnt == 0)
2017-10-01 01:30:53 +02:00
{
delete[] storage_;
storage_ = nullptr;
}
else
size(storage_) = cnt;
// excess elements now unreachable
// note: delete[] knows original size
}
};
}//(End)Implementation helper
using meta::pickArg;
using meta::pickInit;
using meta::IndexSeq;
/**
* Abstraction for path-like topological coordinates.
* A sequence of Literal strings, with array-like access and
* standard iteration. Implemented as fixed size inline tuple
* with heap allocated unlimited extension space.
* @note contents are normalised on initialisation
* - leading empty elements are filled with Symbol::EMPTY
* - empty elements in the middle are replaced by "*"
* - trailing empty elements and "*" are trimmed
*/
template<size_t chunk_size>
class PathArray
{
static_assert (0 < chunk_size, "PathArray chunk_size must be nonempty");
using CcP = const char*;
using LiteralArray = std::array<Literal, chunk_size>;
LiteralArray elms_;
con::Extension tail_;
/**
* @internal delegate ctor to place the initialiser arguments appropriately
* @remarks the two index sequences passed by pattern match determine which
* arguments go to the inline array, and which go to heap allocated extension.
* The inline array has fixed size an is thus filled with trailing `NULL` ptrs,
* which is achieved with the help of meta::pickInit(). The con::Extension
* is an embedded smart-ptr, which, when receiving additional tail arguments,
* will place and manage them within a heap allocated array.
*/
template<size_t...prefix, size_t...rest, typename...ARGS>
PathArray (IndexSeq<prefix...>
,IndexSeq<rest...>
,ARGS&& ...args)
: elms_{pickInit<prefix,CcP> (forward<ARGS>(args)...) ...}
, tail_{pickArg<rest> (forward<ARGS>(args)...) ...}
{
this->normalise();
}
/**
* @internal rebinding helper for building sequences of index numbers,
* to route the initialiser arguments into the corresponding storage
* - the leading (`chunk_size`) arguments go into the LiteralArray inline
* - all the remaining arguments go into heap allocated extension storage
*/
template<typename...ARGS>
struct Split
{
using Prefix = typename meta::BuildIndexSeq<chunk_size>::Ascending;
using Rest = typename meta::BuildIdxIter<ARGS...>::template After<chunk_size>;
};
public:
template<typename...ARGS>
explicit
PathArray (ARGS&& ...args)
: PathArray(typename Split<ARGS...>::Prefix()
,typename Split<ARGS...>::Rest()
,forward<ARGS> (args)...)
{ }
PathArray(PathArray&&) = default;
PathArray(PathArray const&) = default;
PathArray(PathArray& o) : PathArray((PathArray const&)o) { }
PathArray& operator= (PathArray const&) = default;
PathArray& operator= (PathArray &&) = default;
////////////////////////TICKET #963 Forwarding shadows copy operations
size_t
size() const
{
return tail_? chunk_size + tail_.size()
: findInlineEnd() - elms_.begin();
}
size_t
leafLevel() const
{
return empty()? 0 : size()-1;
}
bool
empty() const
{
return not elms_[0]; // normalise() ensures nonnull unless completely empty
}
/** joins nonempty content, separated by slash */
operator string() const;
/** Array style indexed access.
* @throws error::Invalid on bound violation
* @return reference to the normalised content
* @note returns Symbol::EMPTY for leading empty elements,
* even while iteration will skip such entries.
*/
Literal const&
operator[] (size_t idx) const
{
2017-10-01 01:30:53 +02:00
Literal* elm = unConst(this)->getPosition (idx);
if (not elm)
throw error::Invalid ("Accessing index "+util::toString(idx)
+" on PathArray of size "+ util::toString(size())
,error::LUMIERA_ERROR_INDEX_BOUNDS);
return *elm;
}
/** reverse lookup of actual path content
* @param content reference to actual content _residing within the path_
* @return index position of the content within the path
* @throws error::Invalid when the given storage location
* is outside the data content storage of this path
*/
size_t
indexOf (Literal const& content) const
{
if (elms_.begin() <= &content and &content < elms_.end())
return &content - elms_.begin();
if (tail_.isValid (&content))
return chunk_size + tail_.indexOf (&content);
throw error::Invalid ("Referred content "+util::toString(&content)
+" is not located within the storage of PathArray "
+string(*this));
}
protected: /* ==== Iteration control API for IterAdapter ==== */
/** Implementation of Iteration-logic: pull next element. */
friend void
iterNext (const PathArray*, const Literal*& pos)
{
++pos;
}
/** Implementation of Iteration-logic: detect iteration end. */
friend bool
checkPoint (const PathArray* src, const Literal*& pos)
{
REQUIRE (src);
if (pos == src->elms_.end() and src->tail_)
pos = &src->tail_[0];
else
if (not src->isValid (pos))
{
pos = nullptr;
return false;
}
ENSURE ( (src->elms_.begin() <= pos and pos < src->elms_.end())
or src->tail_.isValid(pos));
return true;
}
public:
using const_iterator = lib::IterAdapter<Literal const*, PathArray const*>;
using iterator = const_iterator;
using value_type = Literal;
using reference = Literal&;
using const_reference = Literal const&;
/** @remark iteration is defined to begin with real content */
iterator begin() const { return firstNonempty(); }
iterator end() const { return iterator{}; }
friend iterator begin(PathArray const& pa) { return pa.begin();}
friend iterator end (PathArray const& pa) { return pa.end(); }
2017-10-01 01:30:53 +02:00
2017-10-01 01:30:53 +02:00
private: /* ===== implementation details ===== */
bool
isValid (Literal const* pos) const
{
return pos
and (tail_.isValid(pos)
or (elms_.begin() <= pos and pos < elms_.end()
and *pos));
}
iterator
firstNonempty () const
{
iterator startPos{this, elms_.begin()};
while (startPos && isnil (*startPos))
++startPos;
return startPos;
2017-10-01 01:30:53 +02:00
}
/**
* find _effective end_ of data in the inline array,
* i.e. the position _behind_ the last usable content
*/
Literal const*
findInlineEnd() const
{
Literal const* lastPos = elms_.begin() + chunk_size-1;
Literal const* beforeStart = elms_.begin() - 1;
while (lastPos != beforeStart and not *lastPos)
--lastPos;
return ++lastPos; // at start if empty, else one behind the last
}
/* ===== manipulation by UICoord and Builder subclasses ===== */
protected:
2017-10-01 01:30:53 +02:00
/**
* @internal access content element by index
* @return pointer to storage, `null` if out of bounds
*/
Literal*
getPosition (size_t idx)
{
2017-10-01 01:30:53 +02:00
Literal const* elm =nullptr;
if (idx < chunk_size)
2017-10-01 21:54:35 +02:00
elm = elms_[idx]? &elms_[idx] : nullptr;
2017-10-01 01:30:53 +02:00
else
if (idx-chunk_size < tail_.size())
elm = &tail_[idx-chunk_size];
return const_cast<Literal*> (elm);
}
/**
* @internal ensure storage for the indicated position exists
* @return pointer to the storage, either existing or reallocated
*/
Literal*
expandPosition (size_t idx)
{
if (chunk_size <= idx and size() <= idx)
tail_.resizeTo(idx+1 - chunk_size);
if (idx < chunk_size)
return elms_.begin() + idx;
ENSURE (idx-chunk_size < tail_.size());
return const_cast<Literal*> (&tail_[idx-chunk_size]);
}
2017-10-01 01:30:53 +02:00
/** @internal force new content into the given entry */
void
setContent (Literal* pos, const char* val)
{
REQUIRE (pos);
*reinterpret_cast<const char**> (pos) = val;
}
void
truncateTo (size_t newSize)
{
if (newSize < chunk_size)
{
Literal* end = elms_.end();
Literal* pos = getPosition(newSize);
if (pos)
for ( ; pos!=end; ++pos)
setContent (pos, nullptr);
tail_.resizeTo (0);
}
else
if (newSize-chunk_size < tail_.size())
tail_.resizeTo (newSize - chunk_size);
// otherwise: keep current size, don't grow
}
2017-10-01 01:30:53 +02:00
/**
* establish the _contract_ of PathArray
* - no null pointer within the valid storage range
* - storage is precisely trimmed at the end
* - missing leading elements are filled with Symbol::EMPTY
* - missing inner elements are interpolated as "*"
*/
void
normalise()
{
if (size() == 0) return;
2017-10-01 01:30:53 +02:00
const char* fill = Symbol::EMPTY;
Literal* end = elms_.end();
Literal* pos = elms_.begin();
for ( ; pos!=end; ++pos)
2017-10-01 01:30:53 +02:00
if (isnil (*pos))
setContent (pos, fill);
else
if (fill==Symbol::EMPTY)
fill = Symbol::ANY;
2017-10-01 01:30:53 +02:00
if (tail_)
{
// normalise data in extension storage
pos = getPosition (chunk_size);
end = pos + tail_.size();
for ( ; pos!=end; ++pos)
if (isnil (*pos))
setContent (pos, fill);
else
if (fill==Symbol::EMPTY)
fill = Symbol::ANY;
}
2017-10-01 01:30:53 +02:00
size_t idx = size();
// trim trailing fill
while (idx and fill == *getPosition (--idx))
2017-10-01 01:30:53 +02:00
setContent (getPosition(idx), nullptr);
if (idx >= chunk_size-1)
tail_.resizeTo (idx+1 - chunk_size);
else
tail_.resizeTo (0);
}
};
2017-10-01 01:30:53 +02:00
template<size_t chunk_size>
inline
PathArray<chunk_size>::operator string() const
{
if (this->empty()) return "";
string buff;
size_t expectedLen = this->size() * 10;
2017-10-01 01:30:53 +02:00
buff.reserve (expectedLen); // heuristic
for (Literal elm : *this)
buff += elm + "/";
// chop off last delimiter
size_t len = buff.length();
ASSERT (len >= 1);
buff.resize(len-1);
return buff;
}
/**
* Equality comparison of arbitrary PathArray objects
*/
template<size_t cl, size_t cr>
bool
operator== (PathArray<cl> const& l, PathArray<cr> const& r)
{
if (l.size() != r.size()) return false;
typename PathArray<cl>::iterator lp = l.begin();
typename PathArray<cl>::iterator rp = r.begin();
while (lp and rp)
{
if (*lp != *rp) return false;
++lp;
++rp;
}
return isnil(lp) and isnil(rp);
}
template<size_t cl, size_t cr>
bool
operator!= (PathArray<cl> const& l, PathArray<cr> const& r)
{
return not (l == r);
}
}// namespace lib
#endif /*LIB_PATH_ARRAY_H*/