lumiera_/src/lib/diff/tree-diff-application.hpp
Ichthyostega 289bc7114c implement mutation of the current element (_THIS_)
while implementing this, I've discovered a conceptual error:
we allow to accept attributes, even when we've already entered
the child scope. This means that we can not predictable get back
at the "last" (i.e. the currently touched) element, because this
might be such an attribute. So a really correct implementation
would have to memorise the "current" element, which is really
tricky, given the various ways of touching elements in our
diff language.

In the end I've decided to ignore this problem (maybe a better
solution would have been to disallow those "late" attributes?)
My reasoning is that attributes are unlikely to be full records,
rather just values, and values are never mutated. (but note
that it is definitively possible to have an record as attribute!)
2015-11-01 03:29:35 +01:00

363 lines
13 KiB
C++

/*
TREE-DIFF-APPLICATION.hpp - language to describe differences in linearised form
Copyright (C) Lumiera.org
2014, Hermann Vosseler <Ichthyostega@web.de>
This program 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.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/** @file tree-diff-application.hpp
** Concrete implementation(s) to apply structural changes to hierarchical
** data structures. Together with the generic #DiffApplicator, this allows
** to receive linearised structural diff descriptions and apply them to
** a given target data structure, to effect the corresponding changes.
**
** \par Design considerations
** While -- conceptually -- our tree diff handling can be seen as an extension
** and generalisation of list diffing, the decision was \em not to embody this
** extension into the implementation technically, for sake of clarity. More so,
** since the Record, which serves as foundation for our »External Tree Description«,
** was made to look and behave like a list-like entity, but built with two distinct
** scopes at implementation level: the attribute scope and the contents scope. This
** carries over to the fine points of the list diff language semantics, especially
** when it comes to fault tolerance and strictness vs fuzziness in diff application.
** The implementation is thus faced with having to deal with an internal focus and
** a switch from scope to scope, which adds a lot of complexity. So the list diff
** application strategy can be seen as blueprint and demonstration of principles.
**
** Another point in question is weather see the diff application as manipulating
** a target data structure, or rather building a reshaped copy. The fact that
** GenNode and Record are designed as immutable values seems to favour the latter,
** yet the very reason to engage into building this diff framework was how to
** handle partial updates within a expectedly very large UI model, reflecting
** the actual session model in Proc-Layer. So we end up working on a Mutator,
** which clearly signals we're going to reshape and re-rig the target data.
**
** @see diff-list-application-test.cpp
** @see VerbToken
**
*/
#ifndef LIB_DIFF_TREE_DIFF_APPLICATION_H
#define LIB_DIFF_TREE_DIFF_APPLICATION_H
#include "lib/diff/tree-diff.hpp"
#include "lib/diff/gen-node.hpp"
#include "lib/format-string.hpp"
#include "lib/util.hpp"
#include <utility>
#include <stack>
namespace lib {
namespace diff{
using util::unConst;
using util::_Fmt;
using std::move;
using std::swap;
/**
* Interpreter for the tree-diff-language to work on GenNode elements
* A concrete strategy to apply a structural diff to a target data structure
* made from #Record<GenNode> elements. This data structure is assumed to be
* recursive, tree-like. But because Record elements are conceived as immutable
* and value-like, the tree diff application actually works on a Rec::Mutator
* wrapping the target record to be altered through consuming the diff.
* @throws lumiera::error::State when diff application fails due to the
* target sequence being different than assumed by the given diff.
* @see #TreeDiffInterpreter explanation of the verbs
*/
template<>
class DiffApplicationStrategy<Rec::Mutator>
: public TreeDiffInterpreter
{
using Mutator = Rec::Mutator;
using Content = Rec::ContentMutator;
using Iter = Content::Iter;
struct ScopeFrame
{
Mutator& target;
Content content;
ScopeFrame(Mutator& toModify)
: target(toModify)
, content()
{ }
void init()
{
target.swapContent (content);
}
};
/** Storage: a stack of workspaces
* used to handle nested child objects */
std::stack<ScopeFrame> scopes_;
Mutator& out() { return scopes_.top().target; }
Content& src() { return scopes_.top().content; }
Iter& srcPos() { return scopes_.top().content.pos; }
bool endOfData() { return srcPos() == src().end(); }
Rec& alteredRec() { return out(); }
void
__expect_in_target (GenNode const& elm, Literal oper)
{
if (endOfData())
throw error::State(_Fmt("Unable to %s element %s from target as demanded; "
"no (further) elements in target sequence") % oper % elm
, LUMIERA_ERROR_DIFF_CONFLICT);
if (elm.matches(Ref::CHILD) and not srcPos()->isNamed())
return; // allow for anonymous pick or delete of children
if (not srcPos()->matches(elm))
throw error::State(_Fmt("Unable to %s element %s from target as demanded; "
"found element %s on current target position instead")
% oper % elm % *srcPos()
, LUMIERA_ERROR_DIFF_CONFLICT);
}
void
__expect_further_elements (GenNode const& elm)
{
if (endOfData())
throw error::State(_Fmt("Premature end of target sequence, still expecting element %s; "
"unable to apply diff further.") % elm
, LUMIERA_ERROR_DIFF_CONFLICT);
}
void
__expect_found (GenNode const& elm, Iter const& targetPos)
{
if (targetPos == src().end())
throw error::State(_Fmt("Premature end of sequence; unable to locate "
"element %s in the remainder of the target.") % elm
, LUMIERA_ERROR_DIFF_CONFLICT);
}
void
__expect_successful_location (GenNode const& elm)
{
if (endOfData()
and not ( elm.matches(Ref::END) // after(_END_) -> its OK we hit the end
or (elm.matches(Ref::ATTRIBS) and src().children.empty()))) // after(_ATTRIBS_) -> if there are no children, it's OK to hit the end
throw error::State(_Fmt("Unable locate position 'after(%s)'") % elm.idi
, LUMIERA_ERROR_DIFF_CONFLICT);
}
void
__expect_valid_parent_scope (GenNode::ID const& idi)
{
if (scopes_.empty())
throw error::State(_Fmt("Unbalanced child scope bracketing tokens in diff; "
"When leaving scope %s, we fell out of root scope.") % idi.getSym()
, LUMIERA_ERROR_DIFF_CONFLICT);
if (alteredRec().empty())
throw error::State(_Fmt("Corrupted state. When leaving scope %s, "
"we found an empty parent scope.") % idi.getSym()
, LUMIERA_ERROR_DIFF_CONFLICT);
}
void
__expect_end_of_scope (GenNode::ID const& idi)
{
if (not endOfData())
throw error::State(_Fmt("Incomplete diff: when about to leave scope %s, "
"not all previously existing elements have been confirmed by the diff. "
"At least one spurious element %s was left over") % idi.getSym() % *srcPos()
, LUMIERA_ERROR_DIFF_CONFLICT);
}
Iter
find_in_current_scope (GenNode const& elm)
{
Iter end_of_scope = src().currIsAttrib()? src().attribs.end()
: src().children.end();
return std::find (srcPos(), end_of_scope, elm);
}
GenNode const&
find_child (GenNode::ID const& idi)
{
if (alteredRec().empty())
throw error::State(_Fmt("Attempt to mutate element %s, but current target data scope is empty. "
"Sender and receiver out of sync?") % idi.getSym()
, LUMIERA_ERROR_DIFF_CONFLICT);
// Short-cut-mutation: look at the last element.
// this should be the one just added. BUT NOTE: this fails
// when adding an attribute after entering the child scope.
// Since attributes are typically values and not mutated,
// this inaccuracy was deemed acceptable
auto& current = out().accessLast();
if (Ref::THIS.matches(idi) or current.matches(idi))
return current;
for (auto & child : alteredRec())
if (child.idi == idi)
return child;
throw error::State(_Fmt("Attempt to mutate non existing child record; unable to locate child %s "
"after applying the diff. Current scope: %s") % idi.getSym() % alteredRec()
, LUMIERA_ERROR_DIFF_CONFLICT);
}
void
move_into_new_sequence (Iter pos)
{
if (src().currIsAttrib())
out().appendAttrib (move(*pos)); //////////////TICKET #969 was it a good idea to allow adding attributes "after the fact"?
else
out().appendChild (move(*pos));
}
/* == Implementation of the list diff application primitives == */
void
ins (GenNode const& n) override
{
if (n.isNamed())
if (n.isTypeID())
out().setType (n.data.get<string>());
else
out().appendAttrib(n); //////////////TICKET #969 dto.
else
{
out().appendChild(n);
if (src().currIsAttrib())
src().jumpToChildScope();
}
}
void
del (GenNode const& n) override
{
__expect_in_target(n, "remove");
++src();
}
void
pick (GenNode const& n) override
{
__expect_in_target(n, "pick");
move_into_new_sequence (srcPos());
++src();
}
void
skip (GenNode const& n) override
{
__expect_further_elements (n);
++src();
} // assume the actual content has been moved away by a previous find()
void
find (GenNode const& n) override
{
__expect_further_elements (n);
Iter found = find_in_current_scope(n);
__expect_found (n, found);
move_into_new_sequence (found);
} // consume and leave waste, expected to be cleaned-up by skip() later
/* == Implementation of the tree diff application primitives == */
/** cue to a position behind the named node,
* thereby picking (accepting) all traversed elements
* into the reshaped new data structure as-is */
void
after (GenNode const& n) override
{
if (n.matches(Ref::ATTRIBS))
while (not endOfData() and srcPos()->isNamed())
{
move_into_new_sequence (srcPos());
++src();
}
else
if (n.matches(Ref::END))
while (not endOfData())
{
move_into_new_sequence (srcPos());
++src();
}
else
while (not (endOfData() or srcPos()->matches(n)))
{
move_into_new_sequence (srcPos());
++src();
}
__expect_successful_location(n);
if (not endOfData() and srcPos()->matches(n))
++src(); // get /after/ an explicitly given position
}
/** open nested scope to apply diff to child object */
void
mut (GenNode const& n) override
{
GenNode const& child = find_child (n.idi);
Rec const& childRecord = child.data.get<Rec>();
scopes_.emplace (mutateInPlace (unConst(childRecord)));
}
/** finish and leave child object scope, return to parent */
void
emu (GenNode const& n) override
{
__expect_end_of_scope (n.idi);
scopes_.pop();
__expect_valid_parent_scope (n.idi);
}
public:
explicit
DiffApplicationStrategy(Rec::Mutator& mutableTargetRecord)
: scopes_()
{
scopes_.emplace(mutableTargetRecord);
}
void
initDiffApplication()
{
REQUIRE (1 == scopes_.size());
scopes_.top().init();
}
};
}} // namespace lib::diff
#endif /*LIB_DIFF_TREE_DIFF_APPLICATION_H*/