diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,18 @@
 All notable changes to this project (as seen by library users) will be documented in this file.
 The CHANGELOG is available on [Github](https://github.com/luc-tielen/souffle-haskell.git/CHANGELOG.md).
 
+
+## [3.1.0] - 2021-09-30
+
+### Changed
+
+- souffle-haskell now supports Souffle version 2.1.
+
+### Fixed
+
+- Bug in some C++ assertions that caused the actual assertion message to be
+  wrongly computed.
+
 ## [3.0.0] - 2021-05-03
 
 ### Changed
diff --git a/cbits/souffle.cpp b/cbits/souffle.cpp
--- a/cbits/souffle.cpp
+++ b/cbits/souffle.cpp
@@ -71,11 +71,13 @@
 
 namespace helpers
 {
+using souffle_type = char;
+
 inline auto parse_signature(const souffle::Relation& relation)
 {
     const auto arity = relation.getArity();
 
-    std::vector<char> types;
+    std::vector<souffle_type> types;
     types.reserve(arity);
 
     for (size_t i = 0; i < arity; ++i)
@@ -131,7 +133,7 @@
 }
 
 using deserializer_t = void(*)(souffle::tuple&, char*, offset_t&);
-using deserializer_map = std::unordered_map<char, deserializer_t>;
+using deserializer_map = std::unordered_map<souffle_type, deserializer_t>;
 
 static const deserializer_map deserializers_map = {
     {'s', deserialize_symbol},
@@ -141,7 +143,7 @@
 };
 
 using serializer_t = void(*)(souffle::tuple&, char*, offset_t&);
-using serializer_map = std::unordered_map<char, serializer_t>;
+using serializer_map = std::unordered_map<souffle_type, serializer_t>;
 
 static const serializer_map serializers_map = {
     {'i', serialize_value<number_t>},
@@ -149,16 +151,21 @@
     {'f', serialize_value<float_t>}
 };
 
-inline auto types_to_deserializer(const std::vector<char>& types)
+inline std::string unknown_souffle_type(souffle_type ty)
 {
+    std::string base_message = "Found unknown Souffle primitive type: ";
+    return base_message + std::string(1, ty);
+}
+
+inline auto types_to_deserializer(const std::vector<souffle_type>& types)
+{
     std::vector<deserializer_t> deserializers;
     deserializers.reserve(types.size());
 
     for (const auto& type: types)
     {
         const auto match = deserializers_map.find(type);
-        assert(match != deserializers_map.end() &&
-                ("Found unknown Souffle primitive type: " + match->first));
+        assert(match != deserializers_map.end() && unknown_souffle_type(match->first).c_str());
         deserializers.push_back(match->second);
     }
 
@@ -171,7 +178,7 @@
     };
 }
 
-inline auto types_to_serializer(const std::vector<char>& types)
+inline auto types_to_serializer(const std::vector<souffle_type>& types)
 {
     std::vector<serializer_t> serializers;
     serializers.reserve(types.size());
@@ -179,8 +186,7 @@
     for (const auto& type: types)
     {
         const auto match = serializers_map.find(type);
-        assert(match != serializers_map.end() &&
-                ("Found unknown Souffle primitive type: " + match->first));
+        assert(match != serializers_map.end() && unknown_souffle_type(match->first).c_str());
         serializers.push_back(match->second);
     }
 
@@ -193,7 +199,7 @@
     };
 }
 
-inline auto guess_tuple_size(const std::vector<char>& types)
+inline auto guess_tuple_size(const std::vector<souffle_type>& types)
 {
     size_t size = 0;
 
@@ -229,7 +235,7 @@
     inline const Serializer& serialize()
     {
         using serializer_t = void(*)(Serializer*, souffle::tuple&);
-        using serializer_map_t = std::unordered_map<char, serializer_t>;
+        using serializer_map_t = std::unordered_map<souffle_type, serializer_t>;
 
         serializer_t do_serialize_symbol = [](auto s, auto& t) {
             s->serialize_symbol(t);
@@ -257,8 +263,7 @@
         for (const auto& type: m_types)
         {
             const auto match = serializers_map.find(type);
-            assert(match != serializers_map.end() &&
-                    ("Found unknown Souffle primitive type: " + match->first));
+            assert(match != serializers_map.end() && unknown_souffle_type(match->first).c_str());
             serializers.push_back(match->second);
         }
 
@@ -359,7 +364,7 @@
 
 private:
     const souffle::Relation& m_relation;
-    std::vector<char> m_types;
+    std::vector<souffle_type> m_types;
     size_t m_fact_count;
     buf_data& m_buf;
     size_t m_num_bytes;
diff --git a/cbits/souffle/CompiledSouffle.h b/cbits/souffle/CompiledSouffle.h
--- a/cbits/souffle/CompiledSouffle.h
+++ b/cbits/souffle/CompiledSouffle.h
@@ -16,7 +16,6 @@
 
 #pragma once
 
-#include "souffle/CompiledTuple.h"
 #include "souffle/RamTypes.h"
 #include "souffle/RecordTable.h"
 #include "souffle/SignalHandler.h"
@@ -71,31 +70,39 @@
 /**
  * Relation wrapper used internally in the generated Datalog program
  */
-template <uint32_t id, class RelType, class TupleType, size_t Arity, size_t NumAuxAttributes>
+template <class RelType>
 class RelationWrapper : public souffle::Relation {
+public:
+    static constexpr arity_type Arity = RelType::Arity;
+    using TupleType = Tuple<RamDomain, Arity>;
+    using AttrStrSeq = std::array<const char*, Arity>;
+
 private:
     RelType& relation;
-    SymbolTable& symTable;
+    SouffleProgram& program;
     std::string name;
-    std::array<const char*, Arity> tupleType;
-    std::array<const char*, Arity> tupleName;
+    AttrStrSeq attrTypes;
+    AttrStrSeq attrNames;
+    const uint32_t id;
+    const arity_type numAuxAttribs;
 
+    // NB: internal wrapper. does not satisfy the `iterator` concept.
     class iterator_wrapper : public iterator_base {
         typename RelType::iterator it;
         const Relation* relation;
         tuple t;
 
     public:
-        iterator_wrapper(uint32_t arg_id, const Relation* rel, const typename RelType::iterator& arg_it)
-                : iterator_base(arg_id), it(arg_it), relation(rel), t(rel) {}
+        iterator_wrapper(uint32_t arg_id, const Relation* rel, typename RelType::iterator arg_it)
+                : iterator_base(arg_id), it(std::move(arg_it)), relation(rel), t(rel) {}
         void operator++() override {
             ++it;
         }
         tuple& operator*() override {
+            auto&& value = *it;
             t.rewind();
-            for (size_t i = 0; i < Arity; i++) {
-                t[i] = (*it)[i];
-            }
+            for (std::size_t i = 0; i < Arity; i++)
+                t[i] = value[i];
             return t;
         }
         iterator_base* clone() const override {
@@ -104,26 +111,29 @@
 
     protected:
         bool equal(const iterator_base& o) const override {
-            const auto& casted = static_cast<const iterator_wrapper&>(o);
+            const auto& casted = asAssert<iterator_wrapper>(o);
             return it == casted.it;
         }
     };
 
 public:
-    RelationWrapper(RelType& r, SymbolTable& s, std::string name, const std::array<const char*, Arity>& t,
-            const std::array<const char*, Arity>& n)
-            : relation(r), symTable(s), name(std::move(name)), tupleType(t), tupleName(n) {}
+    RelationWrapper(uint32_t id, RelType& r, SouffleProgram& p, std::string name, const AttrStrSeq& t,
+            const AttrStrSeq& n, arity_type numAuxAttribs)
+            : relation(r), program(p), name(std::move(name)), attrTypes(t), attrNames(n), id(id),
+              numAuxAttribs(numAuxAttribs) {}
+
     iterator begin() const override {
-        return iterator(new iterator_wrapper(id, this, relation.begin()));
+        return iterator(mk<iterator_wrapper>(id, this, relation.begin()));
     }
     iterator end() const override {
-        return iterator(new iterator_wrapper(id, this, relation.end()));
+        return iterator(mk<iterator_wrapper>(id, this, relation.end()));
     }
+
     void insert(const tuple& arg) override {
         TupleType t;
         assert(&arg.getRelation() == this && "wrong relation");
         assert(arg.size() == Arity && "wrong tuple arity");
-        for (size_t i = 0; i < Arity; i++) {
+        for (std::size_t i = 0; i < Arity; i++) {
             t[i] = arg[i];
         }
         relation.insert(t);
@@ -131,7 +141,7 @@
     bool contains(const tuple& arg) const override {
         TupleType t;
         assert(arg.size() == Arity && "wrong tuple arity");
-        for (size_t i = 0; i < Arity; i++) {
+        for (std::size_t i = 0; i < Arity; i++) {
             t[i] = arg[i];
         }
         return relation.contains(t);
@@ -142,22 +152,22 @@
     std::string getName() const override {
         return name;
     }
-    const char* getAttrType(size_t arg) const override {
+    const char* getAttrType(std::size_t arg) const override {
         assert(arg < Arity && "attribute out of bound");
-        return tupleType[arg];
+        return attrTypes[arg];
     }
-    const char* getAttrName(size_t arg) const override {
+    const char* getAttrName(std::size_t arg) const override {
         assert(arg < Arity && "attribute out of bound");
-        return tupleName[arg];
+        return attrNames[arg];
     }
-    size_t getArity() const override {
+    arity_type getArity() const override {
         return Arity;
     }
-    size_t getAuxiliaryArity() const override {
-        return NumAuxAttributes;
+    arity_type getAuxiliaryArity() const override {
+        return numAuxAttribs;
     }
     SymbolTable& getSymbolTable() const override {
-        return symTable;
+        return program.getSymbolTable();
     }
 
     /** Eliminate all the tuples in relation*/
@@ -172,6 +182,8 @@
     std::atomic<bool> data{false};
 
 public:
+    static constexpr Relation::arity_type Arity = 0;
+
     t_nullaries() = default;
     using t_tuple = Tuple<RamDomain, 0>;
     struct context {};
@@ -183,10 +195,10 @@
 
     public:
         typedef std::forward_iterator_tag iterator_category;
-        typedef RamDomain* value_type;
-        typedef ptrdiff_t difference_type;
-        typedef value_type* pointer;
-        typedef value_type& reference;
+        using value_type = RamDomain*;
+        using difference_type = ptrdiff_t;
+        using pointer = value_type*;
+        using reference = value_type&;
 
         iterator(bool v = false) : value(v) {}
 
@@ -247,14 +259,12 @@
     void printStatistics(std::ostream& /* o */) const {}
 };
 
-/** info relations */
-template <int Arity>
+/** Info relations */
+template <Relation::arity_type Arity_>
 class t_info {
-private:
-    std::vector<Tuple<RamDomain, Arity>> data;
-    Lock insert_lock;
-
 public:
+    static constexpr Relation::arity_type Arity = Arity_;
+
     t_info() = default;
     using t_tuple = Tuple<RamDomain, Arity>;
     struct context {};
@@ -303,7 +313,7 @@
     void insert(const RamDomain* ramDomain) {
         insert_lock.lock();
         t_tuple t;
-        for (size_t i = 0; i < Arity; ++i) {
+        for (std::size_t i = 0; i < Arity; ++i) {
             t.data[i] = ramDomain[i];
         }
         data.push_back(t);
@@ -328,6 +338,166 @@
     }
     void purge() {
         data.clear();
+    }
+    void printStatistics(std::ostream& /* o */) const {}
+
+private:
+    std::vector<Tuple<RamDomain, Arity>> data;
+    Lock insert_lock;
+};
+
+/** Equivalence relations */
+struct t_eqrel {
+    static constexpr Relation::arity_type Arity = 2;
+    using t_tuple = Tuple<RamDomain, 2>;
+    using t_ind = EquivalenceRelation<t_tuple>;
+    t_ind ind;
+    class iterator_0 : public std::iterator<std::forward_iterator_tag, t_tuple> {
+        using nested_iterator = typename t_ind::iterator;
+        nested_iterator nested;
+        t_tuple value;
+
+    public:
+        iterator_0() = default;
+        iterator_0(const nested_iterator& iter) : nested(iter), value(*iter) {}
+        iterator_0(const iterator_0& other) = default;
+        iterator_0& operator=(const iterator_0& other) = default;
+        bool operator==(const iterator_0& other) const {
+            return nested == other.nested;
+        }
+        bool operator!=(const iterator_0& other) const {
+            return !(*this == other);
+        }
+        const t_tuple& operator*() const {
+            return value;
+        }
+        const t_tuple* operator->() const {
+            return &value;
+        }
+        iterator_0& operator++() {
+            ++nested;
+            value = *nested;
+            return *this;
+        }
+    };
+    class iterator_1 : public std::iterator<std::forward_iterator_tag, t_tuple> {
+        using nested_iterator = typename t_ind::iterator;
+        nested_iterator nested;
+        t_tuple value;
+
+    public:
+        iterator_1() = default;
+        iterator_1(const nested_iterator& iter) : nested(iter), value(reorder(*iter)) {}
+        iterator_1(const iterator_1& other) = default;
+        iterator_1& operator=(const iterator_1& other) = default;
+        bool operator==(const iterator_1& other) const {
+            return nested == other.nested;
+        }
+        bool operator!=(const iterator_1& other) const {
+            return !(*this == other);
+        }
+        const t_tuple& operator*() const {
+            return value;
+        }
+        const t_tuple* operator->() const {
+            return &value;
+        }
+        iterator_1& operator++() {
+            ++nested;
+            value = reorder(*nested);
+            return *this;
+        }
+    };
+    using iterator = iterator_0;
+    struct context {
+        t_ind::operation_hints hints;
+    };
+    context createContext() {
+        return context();
+    }
+    bool insert(const t_tuple& t) {
+        return ind.insert(t[0], t[1]);
+    }
+    bool insert(const t_tuple& t, context& h) {
+        return ind.insert(t[0], t[1], h.hints);
+    }
+    bool insert(const RamDomain* ramDomain) {
+        RamDomain data[2];
+        std::copy(ramDomain, ramDomain + 2, data);
+        const t_tuple& tuple = reinterpret_cast<const t_tuple&>(data);
+        context h;
+        return insert(tuple, h);
+    }
+    bool insert(RamDomain a1, RamDomain a2) {
+        RamDomain data[2] = {a1, a2};
+        return insert(data);
+    }
+    void extend(const t_eqrel& other) {
+        ind.extend(other.ind);
+    }
+    bool contains(const t_tuple& t) const {
+        return ind.contains(t[0], t[1]);
+    }
+    bool contains(const t_tuple& t, context& h) const {
+        return ind.contains(t[0], t[1]);
+    }
+    std::size_t size() const {
+        return ind.size();
+    }
+    iterator find(const t_tuple& t) const {
+        return ind.find(t);
+    }
+    iterator find(const t_tuple& t, context& h) const {
+        return ind.find(t);
+    }
+    range<iterator> lowerUpperRange_10(const t_tuple& lower, const t_tuple& upper, context& h) const {
+        auto r = ind.template getBoundaries<1>((lower), h.hints);
+        return make_range(iterator(r.begin()), iterator(r.end()));
+    }
+    range<iterator> lowerUpperRange_10(const t_tuple& lower, const t_tuple& upper) const {
+        context h;
+        return lowerUpperRange_10(lower, upper, h);
+    }
+    range<iterator_1> lowerUpperRange_01(const t_tuple& lower, const t_tuple& upper, context& h) const {
+        auto r = ind.template getBoundaries<1>(reorder(lower), h.hints);
+        return make_range(iterator_1(r.begin()), iterator_1(r.end()));
+    }
+    range<iterator_1> lowerUpperRange_01(const t_tuple& lower, const t_tuple& upper) const {
+        context h;
+        return lowerUpperRange_01(lower, upper, h);
+    }
+    range<iterator> lowerUpperRange_11(const t_tuple& lower, const t_tuple& upper, context& h) const {
+        auto r = ind.template getBoundaries<2>((lower), h.hints);
+        return make_range(iterator(r.begin()), iterator(r.end()));
+    }
+    range<iterator> lowerUpperRange_11(const t_tuple& lower, const t_tuple& upper) const {
+        context h;
+        return lowerUpperRange_11(lower, upper, h);
+    }
+    bool empty() const {
+        return ind.size() == 0;
+    }
+    std::vector<range<iterator>> partition() const {
+        std::vector<range<iterator>> res;
+        for (const auto& cur : ind.partition(10000)) {
+            res.push_back(make_range(iterator(cur.begin()), iterator(cur.end())));
+        }
+        return res;
+    }
+    void purge() {
+        ind.clear();
+    }
+    iterator begin() const {
+        return iterator(ind.begin());
+    }
+    iterator end() const {
+        return iterator(ind.end());
+    }
+    static t_tuple reorder(const t_tuple& t) {
+        t_tuple res;
+        res[0] = t[1];
+        res[1] = t[0];
+        return res;
     }
     void printStatistics(std::ostream& /* o */) const {}
 };
diff --git a/cbits/souffle/CompiledTuple.h b/cbits/souffle/CompiledTuple.h
deleted file mode 100644
--- a/cbits/souffle/CompiledTuple.h
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
- * Souffle - A Datalog Compiler
- * Copyright (c) 2013, 2015, Oracle and/or its affiliates. All rights reserved
- * Licensed under the Universal Permissive License v 1.0 as shown at:
- * - https://opensource.org/licenses/UPL
- * - <souffle root>/licenses/SOUFFLE-UPL.txt
- */
-
-/************************************************************************
- *
- * @file CompiledTuple.h
- *
- * The central file covering the data structure utilized by
- * the souffle compiler for representing relations in compiled queries.
- *
- ***********************************************************************/
-
-#pragma once
-
-#include <cstddef>
-#include <functional>
-#include <iostream>
-#include <system_error>
-
-namespace souffle {
-
-/**
- * The type of object stored within relations representing the actual
- * tuple value. Each tuple consists of a constant number of components.
- *
- * @tparam Domain the domain of the component values
- * @tparam arity the number of components within an instance
- */
-template <typename Domain, std::size_t _arity>
-struct Tuple {
-    // some features for template meta programming
-    using value_type = Domain;
-    static constexpr size_t arity = _arity;
-
-    // the stored data
-    Domain data[arity];
-
-    // constructors, destructors and assignment are default
-
-    // provide access to components
-    const Domain& operator[](std::size_t index) const {
-        return data[index];
-    }
-
-    // provide access to components
-    Domain& operator[](std::size_t index) {
-        return data[index];
-    }
-
-    // a comparison operation
-    bool operator==(const Tuple& other) const {
-        for (std::size_t i = 0; i < arity; i++) {
-            if (data[i] != other.data[i]) return false;
-        }
-        return true;
-    }
-
-    // inequality comparison
-    bool operator!=(const Tuple& other) const {
-        return !(*this == other);
-    }
-
-    // required to put tuples into e.g. a std::set container
-    bool operator<(const Tuple& other) const {
-        for (std::size_t i = 0; i < arity; ++i) {
-            if (data[i] < other.data[i]) return true;
-            if (data[i] > other.data[i]) return false;
-        }
-        return false;
-    }
-
-    // required to put tuples into e.g. a btree container
-    bool operator>(const Tuple& other) const {
-        for (std::size_t i = 0; i < arity; ++i) {
-            if (data[i] > other.data[i]) return true;
-            if (data[i] < other.data[i]) return false;
-        }
-        return false;
-    }
-
-    // allow tuples to be printed
-    friend std::ostream& operator<<(std::ostream& out, const Tuple& tuple) {
-        if (arity == 0) return out << "[]";
-        out << "[";
-        for (std::size_t i = 0; i < (std::size_t)(arity - 1); ++i) {
-            out << tuple.data[i];
-            out << ",";
-        }
-        return out << tuple.data[arity - 1] << "]";
-    }
-};
-
-#ifdef _MSC_VER
-/**
- * A template specialization for 0-arity tuples when compiling with microsoft's
- * compiler, because it doesn't like the 0 length array even though it is the
- * last member of the struct.
- */
-template <typename Domain>
-struct Tuple<Domain, 0> {
-    // some features for template meta programming
-    using value_type = Domain;
-    enum { arity = 0 };
-
-    // the stored data
-    Domain data[1];
-
-    // constructores, destructors and assignment are default
-
-    // provide access to components
-    const Domain& operator[](std::size_t index) const {
-        return data[index];
-    }
-
-    // provide access to components
-    Domain& operator[](std::size_t index) {
-        return data[index];
-    }
-
-    // a comparison operation
-    bool operator==(const Tuple& other) const {
-        for (std::size_t i = 0; i < arity; i++) {
-            if (data[i] != other.data[i]) return false;
-        }
-        return true;
-    }
-
-    // inequality comparison
-    bool operator!=(const Tuple& other) const {
-        return !(*this == other);
-    }
-
-    // required to put tuples into e.g. a std::set container
-    bool operator<(const Tuple& other) const {
-        for (std::size_t i = 0; i < arity; ++i) {
-            if (data[i] < other.data[i]) return true;
-            if (data[i] > other.data[i]) return false;
-        }
-        return false;
-    }
-
-    // required to put tuples into e.g. a btree container
-    bool operator>(const Tuple& other) const {
-        for (std::size_t i = 0; i < arity; ++i) {
-            if (data[i] > other.data[i]) return true;
-            if (data[i] < other.data[i]) return false;
-        }
-        return false;
-    }
-
-    // allow tuples to be printed
-    friend std::ostream& operator<<(std::ostream& out, const Tuple& tuple) {
-        if (arity == 0) return out << "[]";
-        out << "[";
-        for (std::size_t i = 0; i < (std::size_t)(arity - 1); ++i) {
-            out << tuple.data[i];
-            out << ",";
-        }
-        return out << tuple.data[arity - 1] << "]";
-    }
-};
-#endif  // _MSC_VER
-}  // end of namespace souffle
-
-// -- add hashing support --
-
-namespace std {
-
-template <typename Domain, std::size_t arity>
-struct hash<souffle::Tuple<Domain, arity>> {
-    size_t operator()(const souffle::Tuple<Domain, arity>& value) const {
-        std::hash<Domain> hash;
-        size_t res = 0;
-        for (unsigned i = 0; i < arity; i++) {
-            // from boost hash combine
-            res ^= hash(value[i]) + 0x9e3779b9 + (res << 6) + (res >> 2);
-        }
-        return res;
-    }
-};
-}  // namespace std
diff --git a/cbits/souffle/RamTypes.h b/cbits/souffle/RamTypes.h
--- a/cbits/souffle/RamTypes.h
+++ b/cbits/souffle/RamTypes.h
@@ -16,6 +16,7 @@
 
 #pragma once
 
+#include <array>
 #include <cstdint>
 #include <cstring>
 #include <iostream>
@@ -23,6 +24,10 @@
 #include <type_traits>
 
 namespace souffle {
+
+// deprecated. use `std::array` directly.
+template <typename A, std::size_t N>
+using Tuple = std::array<A, N>;
 
 /**
  * Types of elements in a tuple.
diff --git a/cbits/souffle/RecordTable.h b/cbits/souffle/RecordTable.h
--- a/cbits/souffle/RecordTable.h
+++ b/cbits/souffle/RecordTable.h
@@ -1,6 +1,6 @@
 /*
  * Souffle - A Datalog Compiler
- * Copyright (c) 2020, The Souffle Developers. All rights reserved.
+ * Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved.
  * Licensed under the Universal Permissive License v 1.0 as shown at:
  * - https://opensource.org/licenses/UPL
  * - <souffle root>/licenses/SOUFFLE-UPL.txt
@@ -17,136 +17,602 @@
 
 #pragma once
 
-#include "souffle/CompiledTuple.h"
 #include "souffle/RamTypes.h"
+#include "souffle/datastructure/ConcurrentFlyweight.h"
+#include "souffle/utility/span.h"
 #include <cassert>
 #include <cstddef>
 #include <limits>
 #include <memory>
-#include <unordered_map>
 #include <utility>
 #include <vector>
 
 namespace souffle {
 
-/** @brief Bidirectional mappping between records and record references */
-class RecordMap {
-    /** arity of record */
-    const size_t arity;
+namespace details {
 
-    /** hash function for unordered record map */
-    struct RecordHash {
-        std::size_t operator()(std::vector<RamDomain> record) const {
-            std::size_t seed = 0;
-            std::hash<RamDomain> domainHash;
-            for (RamDomain value : record) {
-                seed ^= domainHash(value) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
-            }
-            return seed;
+// Helper to unroll for loop
+template <auto Start, auto End, auto Inc, class F>
+constexpr void constexpr_for(F&& f) {
+    if constexpr (Start < End) {
+        f(std::integral_constant<decltype(Start), Start>());
+        constexpr_for<Start + Inc, End, Inc>(f);
+    }
+}
+
+/// @brief The data-type of RamDomain records of any size.
+using GenericRecord = std::vector<RamDomain>;
+
+/// @brief The data-type of RamDomain records of specialized size.
+template <std::size_t Arity>
+using SpecializedRecord = std::array<RamDomain, Arity>;
+
+/// @brief A view in a sequence of RamDomain value.
+// TODO: use a `span`.
+struct GenericRecordView {
+    explicit GenericRecordView(const RamDomain* Data, const std::size_t Arity) : Data(Data), Arity(Arity) {}
+    GenericRecordView(const GenericRecordView& Other) : Data(Other.Data), Arity(Other.Arity) {}
+    GenericRecordView(GenericRecordView&& Other) : Data(Other.Data), Arity(Other.Arity) {}
+
+    const RamDomain* const Data;
+    const std::size_t Arity;
+
+    const RamDomain* data() const {
+        return Data;
+    }
+
+    const RamDomain& operator[](int I) const {
+        assert(I >= 0 && static_cast<std::size_t>(I) < Arity);
+        return Data[I];
+    }
+};
+
+template <std::size_t Arity>
+struct SpecializedRecordView {
+    explicit SpecializedRecordView(const RamDomain* Data) : Data(Data) {}
+    SpecializedRecordView(const SpecializedRecordView& Other) : Data(Other.Data) {}
+    SpecializedRecordView(SpecializedRecordView&& Other) : Data(Other.Data) {}
+
+    const RamDomain* const Data;
+
+    const RamDomain* data() const {
+        return Data;
+    }
+
+    const RamDomain& operator[](int I) const {
+        assert(I >= 0 && static_cast<std::size_t>(I) < Arity);
+        return Data[I];
+    }
+};
+
+/// @brief Hash function object for a RamDomain record.
+struct GenericRecordHash {
+    explicit GenericRecordHash(const std::size_t Arity) : Arity(Arity) {}
+    GenericRecordHash(const GenericRecordHash& Other) : Arity(Other.Arity) {}
+    GenericRecordHash(GenericRecordHash&& Other) : Arity(Other.Arity) {}
+
+    const std::size_t Arity;
+    std::hash<RamDomain> domainHash;
+
+    template <typename T>
+    std::size_t operator()(const T& Record) const {
+        std::size_t Seed = 0;
+        for (std::size_t I = 0; I < Arity; ++I) {
+            Seed ^= domainHash(Record[I]) + 0x9e3779b9 + (Seed << 6) + (Seed >> 2);
         }
+        return Seed;
+    }
+};
+
+template <std::size_t Arity>
+struct SpecializedRecordHash {
+    explicit SpecializedRecordHash() {}
+    SpecializedRecordHash(const SpecializedRecordHash& Other) : DomainHash(Other.DomainHash) {}
+    SpecializedRecordHash(SpecializedRecordHash&& Other) : DomainHash(Other.DomainHash) {}
+
+    std::hash<RamDomain> DomainHash;
+
+    template <typename T>
+    std::size_t operator()(const T& Record) const {
+        std::size_t Seed = 0;
+        constexpr_for<0, Arity, 1>(
+                [&](auto I) { Seed ^= DomainHash(Record[I]) + 0x9e3779b9 + (Seed << 6) + (Seed >> 2); });
+        return Seed;
+    }
+};
+
+template <>
+struct SpecializedRecordHash<0> {
+    explicit SpecializedRecordHash() {}
+    SpecializedRecordHash(const SpecializedRecordHash&) {}
+    SpecializedRecordHash(SpecializedRecordHash&&) {}
+
+    template <typename T>
+    std::size_t operator()(const T&) const {
+        return 0;
+    }
+};
+
+/// @brief Equality function object for RamDomain records.
+struct GenericRecordEqual {
+    explicit GenericRecordEqual(const std::size_t Arity) : Arity(Arity) {}
+    GenericRecordEqual(const GenericRecordEqual& Other) : Arity(Other.Arity) {}
+    GenericRecordEqual(GenericRecordEqual&& Other) : Arity(Other.Arity) {}
+
+    const std::size_t Arity;
+
+    template <typename T, typename U>
+    bool operator()(const T& A, const U& B) const {
+        return (std::memcmp(A.data(), B.data(), Arity * sizeof(RamDomain)) == 0);
+    }
+};
+
+template <std::size_t Arity>
+struct SpecializedRecordEqual {
+    explicit SpecializedRecordEqual() {}
+    SpecializedRecordEqual(const SpecializedRecordEqual&) {}
+    SpecializedRecordEqual(SpecializedRecordEqual&&) {}
+
+    template <typename T, typename U>
+    bool operator()(const T& A, const U& B) const {
+        constexpr std::size_t Len = Arity * sizeof(RamDomain);
+        return (std::memcmp(A.data(), B.data(), Len) == 0);
+    }
+};
+
+template <>
+struct SpecializedRecordEqual<0> {
+    explicit SpecializedRecordEqual() {}
+    SpecializedRecordEqual(const SpecializedRecordEqual&) {}
+    SpecializedRecordEqual(SpecializedRecordEqual&&) {}
+
+    template <typename T, typename U>
+    bool operator()(const T&, const U&) const {
+        return true;
+    }
+};
+
+/// @brief Less function object for RamDomain records.
+struct GenericRecordLess {
+    explicit GenericRecordLess(const std::size_t Arity) : Arity(Arity) {}
+    GenericRecordLess(const GenericRecordLess& Other) : Arity(Other.Arity) {}
+    GenericRecordLess(GenericRecordLess&& Other) : Arity(Other.Arity) {}
+
+    const std::size_t Arity;
+
+    template <typename T, typename U>
+    bool operator()(const T& A, const U& B) const {
+        return (std::memcmp(A.data(), B.data(), Arity * sizeof(RamDomain)) < 0);
+    }
+};
+
+template <std::size_t Arity>
+struct SpecializedRecordLess {
+    explicit SpecializedRecordLess() {}
+    SpecializedRecordLess(const SpecializedRecordLess&) {}
+    SpecializedRecordLess(SpecializedRecordLess&&) {}
+
+    template <typename T, typename U>
+    bool operator()(const T& A, const U& B) const {
+        constexpr std::size_t Len = Arity * sizeof(RamDomain);
+        return (std::memcmp(A.data(), B.data(), Len) < 0);
+    }
+};
+
+template <>
+struct SpecializedRecordLess<0> {
+    explicit SpecializedRecordLess() {}
+    SpecializedRecordLess(const SpecializedRecordLess&) {}
+    SpecializedRecordLess(SpecializedRecordLess&&) {}
+
+    template <typename T, typename U>
+    bool operator()(const T&, const U&) const {
+        return false;
+    }
+};
+
+/// @brief Compare function object for RamDomain records.
+struct GenericRecordCmp {
+    explicit GenericRecordCmp(const std::size_t Arity) : Arity(Arity) {}
+    GenericRecordCmp(const GenericRecordCmp& Other) : Arity(Other.Arity) {}
+    GenericRecordCmp(GenericRecordCmp&& Other) : Arity(Other.Arity) {}
+
+    const std::size_t Arity;
+
+    template <typename T, typename U>
+    int operator()(const T& A, const U& B) const {
+        return std::memcmp(A.data(), B.data(), Arity * sizeof(RamDomain));
+    }
+};
+
+template <std::size_t Arity>
+struct SpecializedRecordCmp {
+    explicit SpecializedRecordCmp() {}
+    SpecializedRecordCmp(const SpecializedRecordCmp&) {}
+    SpecializedRecordCmp(SpecializedRecordCmp&&) {}
+
+    template <typename T, typename U>
+    bool operator()(const T& A, const U& B) const {
+        constexpr std::size_t Len = Arity * sizeof(RamDomain);
+        return std::memcmp(A.data(), B.data(), Len);
+    }
+};
+
+template <>
+struct SpecializedRecordCmp<0> {
+    explicit SpecializedRecordCmp() {}
+    SpecializedRecordCmp(const SpecializedRecordCmp&) {}
+    SpecializedRecordCmp(SpecializedRecordCmp&&) {}
+
+    template <typename T, typename U>
+    bool operator()(const T&, const U&) const {
+        return 0;
+    }
+};
+
+/// @brief Factory of RamDomain record.
+struct GenericRecordFactory {
+    using value_type = GenericRecord;
+    using pointer = GenericRecord*;
+    using reference = GenericRecord&;
+
+    explicit GenericRecordFactory(const std::size_t Arity) : Arity(Arity) {}
+    GenericRecordFactory(const GenericRecordFactory& Other) : Arity(Other.Arity) {}
+    GenericRecordFactory(GenericRecordFactory&& Other) : Arity(Other.Arity) {}
+
+    const std::size_t Arity;
+
+    reference replace(reference Place, const std::vector<RamDomain>& V) {
+        assert(V.size() == Arity);
+        Place = V;
+        return Place;
+    }
+
+    reference replace(reference Place, const GenericRecordView& V) {
+        Place.clear();
+        Place.insert(Place.begin(), V.data(), V.data() + Arity);
+        return Place;
+    }
+
+    reference replace(reference Place, const RamDomain* V) {
+        Place.clear();
+        Place.insert(Place.begin(), V, V + Arity);
+        return Place;
+    }
+};
+
+template <std::size_t Arity>
+struct SpecializedRecordFactory {
+    using value_type = SpecializedRecord<Arity>;
+    using pointer = SpecializedRecord<Arity>*;
+    using reference = SpecializedRecord<Arity>&;
+
+    explicit SpecializedRecordFactory() {}
+    SpecializedRecordFactory(const SpecializedRecordFactory&) {}
+    SpecializedRecordFactory(SpecializedRecordFactory&&) {}
+
+    reference replace(reference Place, const SpecializedRecord<Arity>& V) {
+        assert(V.size() == Arity);
+        Place = V;
+        return Place;
+    }
+
+    reference replace(reference Place, const SpecializedRecordView<Arity>& V) {
+        constexpr std::size_t Len = Arity * sizeof(RamDomain);
+        std::memcpy(Place.data(), V.data(), Len);
+        return Place;
+    }
+
+    reference replace(reference Place, const RamDomain* V) {
+        constexpr std::size_t Len = Arity * sizeof(RamDomain);
+        std::memcpy(Place.data(), V, Len);
+        return Place;
+    }
+};
+
+template <>
+struct SpecializedRecordFactory<0> {
+    using value_type = SpecializedRecord<0>;
+    using pointer = SpecializedRecord<0>*;
+    using reference = SpecializedRecord<0>&;
+
+    explicit SpecializedRecordFactory() {}
+    SpecializedRecordFactory(const SpecializedRecordFactory&) {}
+    SpecializedRecordFactory(SpecializedRecordFactory&&) {}
+
+    reference replace(reference Place, const SpecializedRecord<0>&) {
+        return Place;
+    }
+
+    reference replace(reference Place, const SpecializedRecordView<0>&) {
+        return Place;
+    }
+
+    reference replace(reference Place, const RamDomain*) {
+        return Place;
+    }
+};
+
+}  // namespace details
+
+/** @brief Interface of bidirectional mappping between records and record references. */
+class RecordMap {
+public:
+    virtual ~RecordMap() {}
+    virtual void setNumLanes(const std::size_t NumLanes) = 0;
+    virtual RamDomain pack(const std::vector<RamDomain>& Vector) = 0;
+    virtual RamDomain pack(const RamDomain* Tuple) = 0;
+    virtual RamDomain pack(const std::initializer_list<RamDomain>& List) = 0;
+    virtual const RamDomain* unpack(RamDomain index) const = 0;
+};
+
+/** @brief Bidirectional mappping between records and record references, for any record arity. */
+class GenericRecordMap : public RecordMap,
+                         protected FlyweightImpl<details::GenericRecord, details::GenericRecordHash,
+                                 details::GenericRecordEqual, details::GenericRecordFactory> {
+    using Base = FlyweightImpl<details::GenericRecord, details::GenericRecordHash,
+            details::GenericRecordEqual, details::GenericRecordFactory>;
+
+    const std::size_t Arity;
+
+public:
+    explicit GenericRecordMap(const std::size_t lane_count, const std::size_t arity)
+            : Base(lane_count, 8, true, details::GenericRecordHash(arity), details::GenericRecordEqual(arity),
+                      details::GenericRecordFactory(arity)),
+              Arity(arity) {}
+
+    virtual ~GenericRecordMap() {}
+
+    void setNumLanes(const std::size_t NumLanes) override {
+        Base::setNumLanes(NumLanes);
+    }
+
+    /** @brief converts record to a record reference */
+    RamDomain pack(const std::vector<RamDomain>& Vector) override {
+        return findOrInsert(Vector).first;
     };
 
-    /** map from records to references */
-    // TODO (b-scholz): replace vector<RamDomain> with something more memory-frugal
-    std::unordered_map<std::vector<RamDomain>, RamDomain, RecordHash> recordToIndex;
+    /** @brief converts record to a record reference */
+    RamDomain pack(const RamDomain* Tuple) override {
+        details::GenericRecordView View{Tuple, Arity};
+        return findOrInsert(View).first;
+    }
 
-    /** array of records; index represents record reference */
-    // TODO (b-scholz): replace vector<RamDomain> with something more memory-frugal
-    std::vector<std::vector<RamDomain>> indexToRecord;
+    /** @brief converts record to a record reference */
+    RamDomain pack(const std::initializer_list<RamDomain>& List) override {
+        details::GenericRecordView View{std::data(List), Arity};
+        return findOrInsert(View).first;
+    }
 
+    /** @brief convert record reference to a record pointer */
+    const RamDomain* unpack(RamDomain Index) const override {
+        return fetch(Index).data();
+    }
+};
+
+/** @brief Bidirectional mappping between records and record references, specialized for a record arity. */
+template <std::size_t Arity>
+class SpecializedRecordMap
+        : public RecordMap,
+          protected FlyweightImpl<details::SpecializedRecord<Arity>, details::SpecializedRecordHash<Arity>,
+                  details::SpecializedRecordEqual<Arity>, details::SpecializedRecordFactory<Arity>> {
+    using Record = details::SpecializedRecord<Arity>;
+    using RecordView = details::SpecializedRecordView<Arity>;
+    using RecordHash = details::SpecializedRecordHash<Arity>;
+    using RecordEqual = details::SpecializedRecordEqual<Arity>;
+    using RecordFactory = details::SpecializedRecordFactory<Arity>;
+    using Base = FlyweightImpl<Record, RecordHash, RecordEqual, RecordFactory>;
+
 public:
-    explicit RecordMap(size_t arity) : arity(arity), indexToRecord(1) {}  // note: index 0 element left free
+    SpecializedRecordMap(const std::size_t LaneCount)
+            : Base(LaneCount, 8, true, RecordHash(), RecordEqual(), RecordFactory()) {}
 
+    virtual ~SpecializedRecordMap() {}
+
+    void setNumLanes(const std::size_t NumLanes) override {
+        Base::setNumLanes(NumLanes);
+    }
+
     /** @brief converts record to a record reference */
-    // TODO (b-scholz): replace vector<RamDomain> with something more memory-frugal
-    RamDomain pack(const std::vector<RamDomain>& vector) {
-        RamDomain index;
-#pragma omp critical(record_pack)
-        {
-            auto pos = recordToIndex.find(vector);
-            if (pos != recordToIndex.end()) {
-                index = pos->second;
-            } else {
-#pragma omp critical(record_unpack)
-                {
-                    indexToRecord.push_back(vector);
-                    index = static_cast<RamDomain>(indexToRecord.size()) - 1;
-                    recordToIndex[vector] = index;
+    RamDomain pack(const std::vector<RamDomain>& Vector) override {
+        assert(Vector.size() == Arity);
+        RecordView View{Vector.data()};
+        return Base::findOrInsert(View).first;
+    };
 
-                    // assert that new index is smaller than the range
-                    assert(index != std::numeric_limits<RamDomain>::max());
-                }
-            }
-        }
-        return index;
+    /** @brief converts record to a record reference */
+    RamDomain pack(const RamDomain* Tuple) override {
+        RecordView View{Tuple};
+        return Base::findOrInsert(View).first;
     }
 
-    /** @brief convert record pointer to a record reference */
-    RamDomain pack(const RamDomain* tuple) {
-        // TODO (b-scholz): data is unnecessarily copied
-        // for a successful lookup. To avoid this, we should
-        // compute a hash of the pointer-array and traverse through
-        // the bucket list of the unordered map finding the record.
-        // Note that in case of non-existence, the record still needs to be
-        // copied for the newly created entry but this will be the less
-        // frequent case.
-        std::vector<RamDomain> tmp(arity);
-        for (size_t i = 0; i < arity; i++) {
-            tmp[i] = tuple[i];
-        }
-        return pack(tmp);
+    /** @brief converts record to a record reference */
+    RamDomain pack(const std::initializer_list<RamDomain>& List) override {
+        assert(List.size() == Arity);
+        RecordView View{std::data(List)};
+        return Base::findOrInsert(View).first;
     }
 
     /** @brief convert record reference to a record pointer */
-    const RamDomain* unpack(RamDomain index) const {
-        const RamDomain* res;
-#pragma omp critical(record_unpack)
-        res = indexToRecord[index].data();
-        return res;
+    const RamDomain* unpack(RamDomain Index) const override {
+        return Base::fetch(Index).data();
     }
 };
 
-class RecordTable {
+/** Record map specialized for arity 0 */
+template <>
+class SpecializedRecordMap<0> : public RecordMap {
+    // The empty record always at index 1
+    // The index 0 of each map is reserved.
+    static constexpr RamDomain EmptyRecordIndex = 1;
+
+    // To comply with previous behavior, the empty record
+    // has no data:
+    const RamDomain* EmptyRecordData = nullptr;
+
 public:
-    RecordTable() = default;
-    virtual ~RecordTable() = default;
+    SpecializedRecordMap(const std::size_t /* LaneCount */) {}
 
+    virtual ~SpecializedRecordMap() {}
+
+    void setNumLanes(const std::size_t) override {}
+
+    /** @brief converts record to a record reference */
+    RamDomain pack(const std::vector<RamDomain>& Vector) override {
+        assert(Vector.size() == 0);
+        return EmptyRecordIndex;
+    };
+
+    /** @brief converts record to a record reference */
+    RamDomain pack(const RamDomain*) override {
+        return EmptyRecordIndex;
+    }
+
+    /** @brief converts record to a record reference */
+    RamDomain pack(const std::initializer_list<RamDomain>& List) override {
+        assert(List.size() == 0);
+        return EmptyRecordIndex;
+    }
+
+    /** @brief convert record reference to a record pointer */
+    const RamDomain* unpack(RamDomain Index) const override {
+        assert(Index == EmptyRecordIndex);
+        return EmptyRecordData;
+    }
+};
+
+/** The interface of any Record Table. */
+class RecordTableInterface {
+public:
+    virtual ~RecordTableInterface() {}
+
+    virtual void setNumLanes(const std::size_t NumLanes) = 0;
+
+    virtual RamDomain pack(const RamDomain* Tuple, const std::size_t Arity) = 0;
+
+    virtual const RamDomain* unpack(const RamDomain Ref, const std::size_t Arity) const = 0;
+};
+
+/** A concurrent Record Table with some specialized record maps. */
+template <std::size_t... SpecializedArities>
+class SpecializedRecordTable : public RecordTableInterface {
+private:
+    // The current size of the Maps vector.
+    std::size_t Size;
+
+    // The record maps, indexed by arity.
+    std::vector<RecordMap*> Maps;
+
+    // The concurrency manager.
+    mutable ConcurrentLanes Lanes;
+
+    template <std::size_t Arity, std::size_t... Arities>
+    void CreateSpecializedMaps() {
+        if (Arity >= Size) {
+            Size = Arity + 1;
+            Maps.reserve(Size);
+            Maps.resize(Size);
+        }
+        Maps[Arity] = new SpecializedRecordMap<Arity>(Lanes.lanes());
+        if constexpr (sizeof...(Arities) > 0) {
+            CreateSpecializedMaps<Arities...>();
+        }
+    }
+
+public:
+    /** @brief Construct a record table with the number of concurrent access lanes. */
+    SpecializedRecordTable(const std::size_t LaneCount) : Size(0), Lanes(LaneCount) {
+        CreateSpecializedMaps<SpecializedArities...>();
+    }
+
+    SpecializedRecordTable() : SpecializedRecordTable(1) {}
+
+    virtual ~SpecializedRecordTable() {
+        for (auto Map : Maps) {
+            delete Map;
+        }
+    }
+
+    /**
+     * @brief set the number of concurrent access lanes.
+     * Not thread-safe, use only when the datastructure is not being used.
+     */
+    virtual void setNumLanes(const std::size_t NumLanes) override {
+        Lanes.setNumLanes(NumLanes);
+        for (auto& Map : Maps) {
+            Map->setNumLanes(NumLanes);
+        }
+    }
+
     /** @brief convert record to record reference */
-    RamDomain pack(RamDomain* tuple, size_t arity) {
-        return lookupArity(arity).pack(tuple);
+    virtual RamDomain pack(const RamDomain* Tuple, const std::size_t Arity) override {
+        auto Guard = Lanes.guard();
+        return lookupMap(Arity).pack(Tuple);
     }
+
     /** @brief convert record reference to a record */
-    const RamDomain* unpack(RamDomain ref, size_t arity) const {
-        std::unordered_map<size_t, RecordMap>::const_iterator iter;
-#pragma omp critical(RecordTableGetForArity)
-        {
-            // Find a previously emplaced map
-            iter = maps.find(arity);
-        }
-        assert(iter != maps.end() && "Attempting to unpack record for non-existing arity");
-        return (iter->second).unpack(ref);
+    virtual const RamDomain* unpack(const RamDomain Ref, const std::size_t Arity) const override {
+        auto Guard = Lanes.guard();
+        return lookupMap(Arity).unpack(Ref);
     }
 
 private:
+    /** @brief lookup RecordMap for a given arity; the map for that arity must exist. */
+    RecordMap& lookupMap(const std::size_t Arity) const {
+        assert(Arity < Size && "Lookup for an arity while there is no record for that arity.");
+        auto* Map = Maps[Arity];
+        assert(Map != nullptr && "Lookup for an arity while there is no record for that arity.");
+        return *Map;
+    }
+
     /** @brief lookup RecordMap for a given arity; if it does not exist, create new RecordMap */
-    RecordMap& lookupArity(size_t arity) {
-        std::unordered_map<size_t, RecordMap>::iterator mapsIterator;
-#pragma omp critical(RecordTableGetForArity)
-        {
-            // This will create a new map if it doesn't exist yet.
-            mapsIterator = maps.emplace(arity, arity).first;
+    RecordMap& lookupMap(const std::size_t Arity) {
+        if (Arity < Size) {
+            auto* Map = Maps[Arity];
+            if (Map) {
+                return *Map;
+            }
         }
-        return mapsIterator->second;
+
+        createMap(Arity);
+        return *Maps[Arity];
     }
 
-    /** Arity/RecordMap association */
-    std::unordered_map<size_t, RecordMap> maps;
+    /** @brief create the RecordMap for the given arity. */
+    void createMap(const std::size_t Arity) {
+        Lanes.beforeLockAllBut();
+        if (Arity < Size && Maps[Arity] != nullptr) {
+            // Map of required arity has been created concurrently
+            Lanes.beforeUnlockAllBut();
+            return;
+        }
+        Lanes.lockAllBut();
+
+        if (Arity >= Size) {
+            Size = Arity + 1;
+            Maps.reserve(Size);
+            Maps.resize(Size);
+        }
+        Maps[Arity] = new GenericRecordMap(Lanes.lanes(), Arity);
+
+        Lanes.beforeUnlockAllBut();
+        Lanes.unlockAllBut();
+    }
 };
 
+/** Default record table uses specialized record maps for arities 0 to 12. */
+using RecordTable = SpecializedRecordTable<0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12>;
+
 /** @brief helper to convert tuple to record reference for the synthesiser */
-template <std::size_t Arity>
-inline RamDomain pack(RecordTable& recordTab, Tuple<RamDomain, Arity> tuple) {
-    return recordTab.pack(static_cast<RamDomain*>(tuple.data), Arity);
+template <class RecordTableT, std::size_t Arity>
+RamDomain pack(RecordTableT&& recordTab, Tuple<RamDomain, Arity> const& tuple) {
+    return recordTab.pack(tuple.data(), Arity);
+}
+
+/** @brief helper to convert tuple to record reference for the synthesiser */
+template <class RecordTableT, std::size_t Arity>
+RamDomain pack(RecordTableT&& recordTab, span<const RamDomain, Arity> tuple) {
+    return recordTab.pack(tuple.data(), Arity);
 }
 
 }  // namespace souffle
diff --git a/cbits/souffle/SignalHandler.h b/cbits/souffle/SignalHandler.h
--- a/cbits/souffle/SignalHandler.h
+++ b/cbits/souffle/SignalHandler.h
@@ -21,9 +21,11 @@
 #include <cstdio>
 #include <cstdlib>
 #include <cstring>
+#include <initializer_list>
 #include <iostream>
 #include <mutex>
 #include <string>
+#include <unistd.h>
 
 namespace souffle {
 
@@ -135,6 +137,7 @@
 private:
     // signal context information
     std::atomic<const char*> msg;
+    static_assert(decltype(msg)::is_always_lock_free, "cannot safely use in signal handler");
 
     // state of signal handler
     bool isSet = false;
@@ -150,20 +153,35 @@
      * Signal handler for various types of signals.
      */
     static void handler(int signal) {
-        const char* msg = instance()->msg;
-        std::string error;
+        // Signal handlers have extreme restrictions on what stdlib/OS facilities are available.
+        // This is b/c signals are async on most platforms.
+        // See: https://en.cppreference.com/w/cpp/utility/program/signal
+        // See: `man 7 signal`
+
+        const char* error;
         switch (signal) {
-            case SIGINT: error = "Interrupt"; break;
+            case SIGABRT: error = "Abort"; break;
             case SIGFPE: error = "Floating-point arithmetic exception"; break;
+            case SIGILL: error = "Illegal instruction"; break;
+            case SIGINT: error = "Interrupt"; break;
             case SIGSEGV: error = "Segmentation violation"; break;
+            case SIGTERM: error = "Terminate"; break;
             default: error = "Unknown"; break;
         }
-        if (msg != nullptr) {
-            std::cerr << error << " signal in rule:\n" << msg << std::endl;
-        } else {
-            std::cerr << error << " signal." << std::endl;
-        }
-        exit(1);
+
+        auto write = [](std::initializer_list<char const*> const& msgs) {
+            for (auto&& msg : msgs) {
+                [[maybe_unused]] auto _ = ::write(STDERR_FILENO, msg, ::strlen(msg));
+            }
+        };
+
+        // `instance()` is okay. Static `singleton` must already be constructed if we got here.
+        if (const char* msg = instance()->msg)
+            write({error, " signal in rule:\n", msg, "\n"});
+        else
+            write({error, " signal.\n"});
+
+        std::_Exit(EXIT_FAILURE);
     }
 
     SignalHandler() : msg(nullptr) {}
diff --git a/cbits/souffle/SouffleInterface.h b/cbits/souffle/SouffleInterface.h
--- a/cbits/souffle/SouffleInterface.h
+++ b/cbits/souffle/SouffleInterface.h
@@ -8,7 +8,7 @@
 
 /************************************************************************
  *
- * @file CompiledSouffle.h
+ * @file SouffleInterface.h
  *
  * Main include file for generated C++ classes of Souffle
  *
@@ -17,6 +17,7 @@
 #pragma once
 
 #include "souffle/RamTypes.h"
+#include "souffle/RecordTable.h"
 #include "souffle/SymbolTable.h"
 #include "souffle/utility/MiscUtil.h"
 #include <algorithm>
@@ -27,6 +28,7 @@
 #include <iostream>
 #include <map>
 #include <memory>
+#include <optional>
 #include <string>
 #include <tuple>
 #include <utility>
@@ -40,6 +42,9 @@
  * Object-oriented wrapper class for Souffle's templatized relations.
  */
 class Relation {
+public:
+    using arity_type = uint32_t;
+
 protected:
     /**
      * Abstract iterator class.
@@ -57,6 +62,9 @@
          * Required for identifying type of iterator
          * (NB: LLVM has no typeinfo).
          *
+         * Note: The above statement is not true anymore - should this be made to work the same
+         * as Node::operator==?
+         *
          * TODO (Honghyw) : Provide a clear documentation of what id is used for.
          */
         uint32_t id;
@@ -151,7 +159,7 @@
          * iterator_base class pointer.
          *
          */
-        Own<iterator_base> iter = nullptr;
+        std::unique_ptr<iterator_base> iter = nullptr;
 
     public:
         /**
@@ -177,7 +185,7 @@
          *
          * @param arg An iterator_base class pointer
          */
-        iterator(iterator_base* arg) : iter(arg) {}
+        iterator(std::unique_ptr<iterator_base> it) : iter(std::move(it)) {}
 
         /**
          * Destructor.
@@ -224,6 +232,20 @@
         }
 
         /**
+         * Overload the "++" operator.
+         *
+         * Copies the iterator, increments itself, and returns the (pre-increment) copy.
+         * WARNING: Expensive due to copy! Included for API compatibility.
+         *
+         * @return Pre-increment copy of `this`.
+         */
+        iterator operator++(int) {
+            auto cpy = *this;
+            ++(*this);
+            return cpy;
+        }
+
+        /**
          * Overload the "*" operator.
          *
          * This will return the tuple that the iterator is pointing to.
@@ -314,39 +336,52 @@
      * which are the primitive types in Souffle.
      * <type name> is the name given by the user in the Souffle program
      *
-     * @param The index of the column starting starting from 0 (size_t)
+     * @param The index of the column starting starting from 0 (std::size_t)
      * @return The constant string of the attribute type
      */
-    virtual const char* getAttrType(size_t) const = 0;
+    virtual const char* getAttrType(std::size_t) const = 0;
 
     /**
      * Get the attribute name of a relation at the column specified by the parameter.
      * The attribute name is the name given to the type by the user in the .decl statement. For example, for
      * ".decl edge (node1:Node, node2:Node)", the attribute names are node1 and node2.
      *
-     * @param The index of the column starting starting from 0 (size_t)
+     * @param The index of the column starting starting from 0 (std::size_t)
      * @return The constant string of the attribute name
      */
-    virtual const char* getAttrName(size_t) const = 0;
+    virtual const char* getAttrName(std::size_t) const = 0;
 
     /**
      * Return the arity of a relation.
      * For example for a tuple (1 2) the arity is 2 and for a tuple (1 2 3) the arity is 3.
      *
-     * @return Arity of a relation (size_t)
+     * @return Arity of a relation (`arity_type`)
      */
-    virtual size_t getArity() const = 0;
+    virtual arity_type getArity() const = 0;
 
     /**
      * Return the number of auxiliary attributes. Auxiliary attributes
      * are used for provenance and and other alternative evaluation
      * strategies. They are stored as the last attributes of a tuple.
      *
-     * @return Number of auxiliary attributes of a relation (size_t)
+     * @return Number of auxiliary attributes of a relation (`arity_type`)
      */
-    virtual size_t getAuxiliaryArity() const = 0;
+    virtual arity_type getAuxiliaryArity() const = 0;
 
     /**
+     * Return the number of non-auxiliary attributes.
+     * Auxiliary attributes are used for provenance and and other alternative
+     * evaluation strategies.
+     * They are stored as the last attributes of a tuple.
+     *
+     * @return Number of non-auxiliary attributes of a relation (`arity_type`)
+     */
+    arity_type getPrimaryArity() const {
+        assert(getAuxiliaryArity() <= getArity());
+        return getArity() - getAuxiliaryArity();
+    }
+
+    /**
      * Get the symbol table of a relation.
      * The symbols in a tuple to be stored into a relation are stored and assigned with a number in a table
      * called symbol table. For example, to insert ("John","Student") to a relation, "John" and "Student" are
@@ -374,7 +409,7 @@
         }
 
         std::string signature = "<" + std::string(getAttrType(0));
-        for (size_t i = 1; i < getArity(); i++) {
+        for (arity_type i = 1; i < getArity(); i++) {
             signature += "," + std::string(getAttrType(i));
         }
         signature += ">";
@@ -423,7 +458,7 @@
      * helps to make sure we access an insert a tuple within the bound by making sure pos never exceeds the
      * arity of the relation.
      */
-    size_t pos;
+    std::size_t pos;
 
 public:
     /**
@@ -472,10 +507,11 @@
     /**
      * Return the number of elements in the tuple.
      *
-     * @return the number of elements in the tuple (size_t).
+     * @return the number of elements in the tuple (std::size_t).
      */
-    size_t size() const {
-        return array.size();
+    Relation::arity_type size() const {
+        assert(array.size() <= std::numeric_limits<Relation::arity_type>::max());
+        return Relation::arity_type(array.size());
     }
 
     /**
@@ -488,9 +524,9 @@
      * only be used by friendly classes such as
      * iterators; users should not use this interface.
      *
-     * @param idx This is the idx of element in a tuple (size_t).
+     * @param idx This is the idx of element in a tuple (std::size_t).
      */
-    RamDomain& operator[](size_t idx) {
+    RamDomain& operator[](std::size_t idx) {
         return array[idx];
     }
 
@@ -504,9 +540,9 @@
      * only be used by friendly classes such as
      * iterators; users should not use this interface.
      *
-     * @param idx This is the idx of element in a tuple (size_t).
+     * @param idx This is the idx of element in a tuple (std::size_t).
      */
-    const RamDomain& operator[](size_t idx) const {
+    const RamDomain& operator[](std::size_t idx) const {
         return array[idx];
     }
 
@@ -527,7 +563,7 @@
     tuple& operator<<(const std::string& str) {
         assert(pos < size() && "exceeded tuple's size");
         assert(*relation.getAttrType(pos) == 's' && "wrong element type");
-        array[pos++] = relation.getSymbolTable().lookup(str);
+        array[pos++] = relation.getSymbolTable().encode(str);
         return *this;
     }
 
@@ -584,7 +620,7 @@
     tuple& operator>>(std::string& str) {
         assert(pos < size() && "exceeded tuple's size");
         assert(*relation.getAttrType(pos) == 's' && "wrong element type");
-        str = relation.getSymbolTable().resolve(array[pos++]);
+        str = relation.getSymbolTable().decode(array[pos++]);
         return *this;
     }
 
@@ -694,24 +730,30 @@
      * otherwise will add to internalRelations. (a relation could be both input and output at the same time.)
      *
      * @param name the name of the relation (std::string)
-     * @param rel a pointer to the relation (std::string)
+     * @param rel a reference to the relation
      * @param isInput a bool argument, true if the relation is a input relation, else false (bool)
      * @param isOnput a bool argument, true if the relation is a ouput relation, else false (bool)
      */
-    void addRelation(const std::string& name, Relation* rel, bool isInput, bool isOutput) {
-        relationMap[name] = rel;
-        allRelations.push_back(rel);
+    void addRelation(const std::string& name, Relation& rel, bool isInput, bool isOutput) {
+        relationMap[name] = &rel;
+        allRelations.push_back(&rel);
         if (isInput) {
-            inputRelations.push_back(rel);
+            inputRelations.push_back(&rel);
         }
         if (isOutput) {
-            outputRelations.push_back(rel);
+            outputRelations.push_back(&rel);
         }
         if (!isInput && !isOutput) {
-            internalRelations.push_back(rel);
+            internalRelations.push_back(&rel);
         }
     }
 
+    [[deprecated("pass `rel` by reference; `rel` may not be null"), maybe_unused]] void addRelation(
+            const std::string& name, Relation* rel, bool isInput, bool isOutput) {
+        assert(rel && "`rel` may not be null");
+        addRelation(name, *rel, isInput, isOutput);
+    }
+
 public:
     /**
      * Destructor.
@@ -763,7 +805,7 @@
     /**
      * Set the number of threads to be used
      */
-    void setNumThreads(std::size_t numThreadsValue) {
+    virtual void setNumThreads(std::size_t numThreadsValue) {
         this->numThreads = numThreadsValue;
     }
 
@@ -795,8 +837,12 @@
      * @param name The name of the target relation (const std::string)
      * @return The size of the target relation (std::size_t)
      */
-    std::size_t getRelationSize(const std::string& name) const {
-        return getRelation(name)->size();
+    std::optional<std::size_t> getRelationSize(const std::string& name) const {
+        if (auto* rel = getRelation(name)) {
+            return rel->size();
+        }
+
+        return std::nullopt;
     }
 
     /**
@@ -805,8 +851,12 @@
      * @param name The name of the target relation (const std::string)
      * @return The name of the target relation (std::string)
      */
-    std::string getRelationName(const std::string& name) const {
-        return getRelation(name)->getName();
+    std::optional<std::string> getRelationName(const std::string& name) const {
+        if (auto* rel = getRelation(name)) {
+            return rel->getName();
+        }
+
+        return std::nullopt;
     }
 
     /**
@@ -867,6 +917,11 @@
     virtual SymbolTable& getSymbolTable() = 0;
 
     /**
+     * Get the record table of the program.
+     */
+    virtual RecordTable& getRecordTable() = 0;
+
+    /**
      * Remove all the tuples from the outputRelations, calling the purge method of each.
      *
      * @see Relation::purge()
@@ -902,7 +957,7 @@
     /**
      * Helper function for the wrapper function Relation::insert() and Relation::contains().
      */
-    template <typename Tuple, size_t N>
+    template <typename Tuple, std::size_t N>
     struct tuple_insert {
         static void add(const Tuple& t, souffle::tuple& t1) {
             tuple_insert<Tuple, N - 1>::add(t, t1);
diff --git a/cbits/souffle/SymbolTable.h b/cbits/souffle/SymbolTable.h
--- a/cbits/souffle/SymbolTable.h
+++ b/cbits/souffle/SymbolTable.h
@@ -1,6 +1,6 @@
 /*
  * Souffle - A Datalog Compiler
- * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved
+ * Copyright (c) 2013, 2014, 2015 Oracle and/or its affiliates. All rights reserved
  * Licensed under the Universal Permissive License v 1.0 as shown at:
  * - https://opensource.org/licenses/UPL
  * - <souffle root>/licenses/SOUFFLE-UPL.txt
@@ -10,13 +10,14 @@
  *
  * @file SymbolTable.h
  *
- * Data container to store symbols of the Datalog program.
+ * Encodes/decodes symbols to numbers (and vice versa).
  *
  ***********************************************************************/
 
 #pragma once
 
 #include "souffle/RamTypes.h"
+#include "souffle/datastructure/ConcurrentFlyweight.h"
 #include "souffle/utility/MiscUtil.h"
 #include "souffle/utility/ParallelUtil.h"
 #include "souffle/utility/StreamUtil.h"
@@ -35,208 +36,86 @@
 /**
  * @class SymbolTable
  *
- * Global pool of re-usable strings
- *
- * SymbolTable stores Datalog symbols and converts them to numbers and vice versa.
+ * SymbolTable encodes symbols to numbers and decodes numbers to symbols.
  */
-class SymbolTable {
+class SymbolTable : protected FlyweightImpl<std::string> {
 private:
-    /** A lock to synchronize parallel accesses */
-    mutable Lock access;
-
-    /** Map indices to strings. */
-    std::deque<std::string> numToStr;
-
-    /** Map strings to indices. */
-    std::unordered_map<std::string, size_t> strToNum;
-
-    /** Convenience method to place a new symbol in the table, if it does not exist, and return the index of
-     * it. */
-    inline size_t newSymbolOfIndex(const std::string& symbol) {
-        size_t index;
-        auto it = strToNum.find(symbol);
-        if (it == strToNum.end()) {
-            index = numToStr.size();
-            strToNum[symbol] = index;
-            numToStr.push_back(symbol);
-        } else {
-            index = it->second;
-        }
-        return index;
-    }
-
-    /** Convenience method to place a new symbol in the table, if it does not exist. */
-    inline void newSymbol(const std::string& symbol) {
-        if (strToNum.find(symbol) == strToNum.end()) {
-            strToNum[symbol] = numToStr.size();
-            numToStr.push_back(symbol);
-        }
-    }
+    using Base = FlyweightImpl<std::string>;
 
 public:
-    /** Empty constructor. */
-    SymbolTable() = default;
-
-    /** Copy constructor, performs a deep copy. */
-    SymbolTable(const SymbolTable& other) : numToStr(other.numToStr), strToNum(other.strToNum) {}
+    using iterator = typename Base::iterator;
 
-    /** Copy constructor for r-value reference. */
-    SymbolTable(SymbolTable&& other) noexcept {
-        numToStr.swap(other.numToStr);
-        strToNum.swap(other.strToNum);
-    }
+    /** @brief Construct a symbol table with the given number of concurrent access lanes. */
+    SymbolTable(const std::size_t LaneCount = 1) : Base(LaneCount) {}
 
-    SymbolTable(std::initializer_list<std::string> symbols) {
-        strToNum.reserve(symbols.size());
+    /** @brief Construct a symbol table with the given initial symbols. */
+    SymbolTable(std::initializer_list<std::string> symbols) : Base(1, symbols.size()) {
         for (const auto& symbol : symbols) {
-            newSymbol(symbol);
-        }
-    }
-
-    /** Destructor, frees memory allocated for all strings. */
-    virtual ~SymbolTable() = default;
-
-    /** Assignment operator, performs a deep copy and frees memory allocated for all strings. */
-    SymbolTable& operator=(const SymbolTable& other) {
-        if (this == &other) {
-            return *this;
-        }
-        numToStr = other.numToStr;
-        strToNum = other.strToNum;
-        return *this;
-    }
-
-    /** Assignment operator for r-value references. */
-    SymbolTable& operator=(SymbolTable&& other) noexcept {
-        numToStr.swap(other.numToStr);
-        strToNum.swap(other.strToNum);
-        return *this;
-    }
-
-    /** Find the index of a symbol in the table, inserting a new symbol if it does not exist there
-     * already. */
-    RamDomain lookup(const std::string& symbol) {
-        {
-            auto lease = access.acquire();
-            (void)lease;  // avoid warning;
-            return static_cast<RamDomain>(newSymbolOfIndex(symbol));
-        }
-    }
-
-    /** Finds the index of a symbol in the table, giving an error if it's not found */
-    RamDomain lookupExisting(const std::string& symbol) const {
-        {
-            auto lease = access.acquire();
-            (void)lease;  // avoid warning;
-            auto result = strToNum.find(symbol);
-            if (result == strToNum.end()) {
-                fatal("Error string not found in call to `SymbolTable::lookupExisting`: `%s`", symbol);
-            }
-            return static_cast<RamDomain>(result->second);
+            findOrInsert(symbol);
         }
     }
 
-    /** Find the index of a symbol in the table, inserting a new symbol if it does not exist there
-     * already. */
-    RamDomain unsafeLookup(const std::string& symbol) {
-        return static_cast<RamDomain>(newSymbolOfIndex(symbol));
-    }
-
-    /** Find a symbol in the table by its index, note that this gives an error if the index is out of
-     * bounds.
+    /** @brief Construct a symbol table with the given number of concurrent access lanes and initial symbols.
      */
-    const std::string& resolve(const RamDomain index) const {
-        {
-            auto lease = access.acquire();
-            (void)lease;  // avoid warning;
-            auto pos = static_cast<size_t>(index);
-            if (pos >= size()) {
-                // TODO: use different error reporting here!!
-                fatal("Error index out of bounds in call to `SymbolTable::resolve`. index = `%d`", index);
-            }
-            return numToStr[pos];
+    SymbolTable(const std::size_t LaneCount, std::initializer_list<std::string> symbols)
+            : Base(LaneCount, symbols.size()) {
+        for (const auto& symbol : symbols) {
+            findOrInsert(symbol);
         }
     }
 
-    const std::string& unsafeResolve(const RamDomain index) const {
-        return numToStr[static_cast<size_t>(index)];
+    /**
+     * @brief Set the number of concurrent access lanes.
+     * This function is not thread-safe, do not call when other threads are using the datastructure.
+     */
+    void setNumLanes(const std::size_t NumLanes) {
+        Base::setNumLanes(NumLanes);
     }
 
-    /* Return the size of the symbol table, being the number of symbols it currently holds. */
-    size_t size() const {
-        return numToStr.size();
+    /** @brief Return an iterator on the first symbol. */
+    iterator begin() const {
+        return Base::begin();
     }
 
-    /** Bulk insert symbols into the table, note that this operation is more efficient than repeated
-     * inserts
-     * of single symbols. */
-    void insert(const std::vector<std::string>& symbols) {
-        {
-            auto lease = access.acquire();
-            (void)lease;  // avoid warning;
-            strToNum.reserve(size() + symbols.size());
-            for (auto& symbol : symbols) {
-                newSymbol(symbol);
-            }
-        }
+    /** @brief Return an iterator past the last symbol. */
+    iterator end() const {
+        return Base::end();
     }
 
-    /** Insert a single symbol into the table, not that this operation should not be used if inserting
-     * symbols
-     * in bulk. */
-    void insert(const std::string& symbol) {
-        {
-            auto lease = access.acquire();
-            (void)lease;  // avoid warning;
-            newSymbol(symbol);
-        }
+    /** @brief Check if the given symbol exist. */
+    bool weakContains(const std::string& symbol) const {
+        return Base::weakContains(symbol);
     }
 
-    /** Print the symbol table to the given stream. */
-    void print(std::ostream& out) const {
-        {
-            out << "SymbolTable: {\n\t";
-            out << join(strToNum, "\n\t",
-                           [](std::ostream& out, const std::pair<std::string, std::size_t>& entry) {
-                               out << entry.first << "\t => " << entry.second;
-                           })
-                << "\n";
-            out << "}\n";
-        }
+    /** @brief Encode a symbol to a symbol index. */
+    RamDomain encode(const std::string& symbol) {
+        return Base::findOrInsert(symbol).first;
     }
 
-    /** Check if the symbol table contains a string */
-    bool contains(const std::string& symbol) const {
-        auto lease = access.acquire();
-        (void)lease;  // avoid warning;
-        auto result = strToNum.find(symbol);
-        if (result == strToNum.end()) {
-            return false;
-        } else {
-            return true;
-        }
+    /** @brief Decode a symbol index to a symbol. */
+    const std::string& decode(const RamDomain index) const {
+        return Base::fetch(index);
     }
 
-    /** Check if the symbol table contains an index */
-    bool contains(const RamDomain index) const {
-        auto lease = access.acquire();
-        (void)lease;  // avoid warning;
-        auto pos = static_cast<size_t>(index);
-        if (pos >= size()) {
-            return false;
-        } else {
-            return true;
-        }
+    /** @brief Encode a symbol to a symbol index; aliases encode. */
+    RamDomain unsafeEncode(const std::string& symbol) {
+        return encode(symbol);
     }
 
-    Lock::Lease acquireLock() const {
-        return access.acquire();
+    /** @brief Decode a symbol index to a symbol; aliases decode. */
+    const std::string& unsafeDecode(const RamDomain index) const {
+        return decode(index);
     }
 
-    /** Stream operator, used as a convenience for print. */
-    friend std::ostream& operator<<(std::ostream& out, const SymbolTable& table) {
-        table.print(out);
-        return out;
+    /**
+     * @brief Encode the symbol, it is inserted if it does not exist.
+     *
+     * @return the symbol index and a boolean indicating if an insertion
+     * happened.
+     */
+    std::pair<RamDomain, bool> findOrInsert(const std::string& symbol) {
+        auto Res = Base::findOrInsert(symbol);
+        return std::make_pair(Res.first, Res.second);
     }
 };
 
diff --git a/cbits/souffle/datastructure/BTree.h b/cbits/souffle/datastructure/BTree.h
--- a/cbits/souffle/datastructure/BTree.h
+++ b/cbits/souffle/datastructure/BTree.h
@@ -19,6 +19,7 @@
 
 #include "souffle/utility/CacheUtil.h"
 #include "souffle/utility/ContainerUtil.h"
+#include "souffle/utility/MiscUtil.h"
 #include "souffle/utility/ParallelUtil.h"
 #include <algorithm>
 #include <cassert>
@@ -367,13 +368,13 @@
         /**
          * The number of keys/node desired by the user.
          */
-        static constexpr size_t desiredNumKeys =
+        static constexpr std::size_t desiredNumKeys =
                 ((blockSize > sizeof(base)) ? blockSize - sizeof(base) : 0) / sizeof(Key);
 
         /**
          * The actual number of keys/node corrected by functional requirements.
          */
-        static constexpr size_t maxKeys = (desiredNumKeys > 3) ? desiredNumKeys : 3;
+        static constexpr std::size_t maxKeys = (desiredNumKeys > 3) ? desiredNumKeys : 3;
 
         // the keys stored in this node
         Key keys[maxKeys];
@@ -1105,11 +1106,11 @@
         field_index_type pos = 0;
 
     public:
-        typedef std::forward_iterator_tag iterator_category;
-        typedef Key value_type;
-        typedef ptrdiff_t difference_type;
-        typedef value_type* pointer;
-        typedef value_type& reference;
+        using iterator_category = std::forward_iterator_tag;
+        using value_type = Key;
+        using difference_type = ptrdiff_t;
+        using pointer = value_type*;
+        using reference = value_type&;
 
         // default constructor -- creating an end-iterator
         iterator() : cur(nullptr) {}
@@ -1256,7 +1257,7 @@
 
 public:
     // the maximum number of keys stored per node
-    static constexpr size_t max_keys_per_node = node::maxKeys;
+    static constexpr std::size_t max_keys_per_node = node::maxKeys;
 
     // -- ctors / dtors --
 
diff --git a/cbits/souffle/datastructure/Brie.h b/cbits/souffle/datastructure/Brie.h
--- a/cbits/souffle/datastructure/Brie.h
+++ b/cbits/souffle/datastructure/Brie.h
@@ -26,3151 +26,3048 @@
 
 #pragma once
 
-#include "souffle/CompiledTuple.h"
-#include "souffle/RamTypes.h"
-#include "souffle/utility/CacheUtil.h"
-#include "souffle/utility/ContainerUtil.h"
-#include "souffle/utility/StreamUtil.h"
-#include <algorithm>
-#include <atomic>
-#include <bitset>
-#include <cassert>
-#include <cstdint>
-#include <cstring>
-#include <iostream>
-#include <iterator>
-#include <limits>
-#include <utility>
-#include <vector>
-
-#ifdef _WIN32
-/**
- * When compiling for windows, redefine the gcc builtins which are used to
- * their equivalents on the windows platform.
- */
-#define __sync_synchronize MemoryBarrier
-#define __sync_bool_compare_and_swap(ptr, oldval, newval) \
-    (InterlockedCompareExchangePointer((void* volatile*)ptr, (void*)newval, (void*)oldval) == (void*)oldval)
-#endif  // _WIN32
-
-namespace souffle {
-
-namespace detail {
-
-/**
- * A templated functor to obtain default values for
- * unspecified elements of sparse array instances.
- */
-template <typename T>
-struct default_factory {
-    T operator()() const {
-        return T();  // just use the default constructor
-    }
-};
-
-/**
- * A functor representing the identity function.
- */
-template <typename T>
-struct identity {
-    T operator()(T v) const {
-        return v;
-    }
-};
-
-/**
- * A operation to be utilized by the sparse map when merging
- * elements associated to different values.
- */
-template <typename T>
-struct default_merge {
-    /**
-     * Merges two values a and b when merging spase maps.
-     */
-    T operator()(T a, T b) const {
-        default_factory<T> def;
-        // if a is the default => us b, else stick to a
-        return (a != def()) ? a : b;
-    }
-};
-
-}  // end namespace detail
-
-/**
- * A sparse array simulates an array associating to every element
- * of uint32_t an element of a generic type T. Any non-defined element
- * will be default-initialized utilizing the detail::default_factory
- * functor.
- *
- * Internally the array is organized as a balanced tree. The leaf
- * level of the tree corresponds to the elements of the represented
- * array. Inner nodes utilize individual bits of the indices to reference
- * sub-trees. For efficiency reasons, only the minimal sub-tree required
- * to cover all non-null / non-default values stored in the array is
- * maintained. Furthermore, several levels of nodes are aggreated in a
- * B-tree like fashion to inprove cache utilization and reduce the number
- * of steps required for lookup and insert operations.
- *
- * @tparam T the type of the stored elements
- * @tparam BITS the number of bits consumed per node-level
- *              e.g. if it is set to 3, the resulting tree will be of a degree of
- *              2^3=8, and thus 8 child-pointers will be stored in each inner node
- *              and as many values will be stored in each leaf node.
- * @tparam merge_op the functor to be utilized when merging the content of two
- *              instances of this type.
- * @tparam copy_op a functor to be applied to each stored value when copying an
- *              instance of this array. For instance, this is utilized by the
- *              trie implementation to create a clone of each sub-tree instead
- *              of preserving the original pointer.
- */
-template <typename T, unsigned BITS = 6, typename merge_op = detail::default_merge<T>,
-        typename copy_op = detail::identity<T>>
-class SparseArray {
-    using key_type = uint64_t;
-
-    // some internal constants
-    static constexpr int BIT_PER_STEP = BITS;
-    static constexpr int NUM_CELLS = 1 << BIT_PER_STEP;
-    static constexpr key_type INDEX_MASK = NUM_CELLS - 1;
-
-public:
-    // the type utilized for indexing contained elements
-    using index_type = key_type;
-
-    // the type of value stored in this array
-    using value_type = T;
-
-    // the atomic view on stored values
-    using atomic_value_type = std::atomic<value_type>;
-
-private:
-    struct Node;
-
-    /**
-     * The value stored in a single cell of a inner
-     * or leaf node.
-     */
-    union Cell {
-        // an atomic view on the pointer referencing a nested level
-        std::atomic<Node*> aptr;
-
-        // a pointer to the nested level (unsynchronized operations)
-        Node* ptr{nullptr};
-
-        // an atomic view on the value stored in this cell (leaf node)
-        atomic_value_type avalue;
-
-        // the value stored in this cell (unsynchronized access, leaf node)
-        value_type value;
-    };
-
-    /**
-     * The node type of the internally maintained tree.
-     */
-    struct Node {
-        // a pointer to the parent node (for efficient iteration)
-        const Node* parent;
-        // the pointers to the child nodes (inner nodes) or the stored values (leaf nodes)
-        Cell cell[NUM_CELLS];
-    };
-
-    /**
-     * A struct describing all the information required by the container
-     * class to manage the wrapped up tree.
-     */
-    struct RootInfo {
-        // the root node of the tree
-        Node* root;
-        // the number of levels of the tree
-        uint32_t levels;
-        // the absolute offset of the theoretical first element in the tree
-        index_type offset;
-
-        // the first leaf node in the tree
-        Node* first;
-        // the absolute offset of the first element in the first leaf node
-        index_type firstOffset;
-    };
-
-    union {
-        RootInfo unsynced;         // for sequential operations
-        volatile RootInfo synced;  // for synchronized operations
-    };
-
-public:
-    /**
-     * A default constructor creating an empty sparse array.
-     */
-    SparseArray() : unsynced(RootInfo{nullptr, 0, 0, nullptr, std::numeric_limits<index_type>::max()}) {}
-
-    /**
-     * A copy constructor for sparse arrays. It creates a deep
-     * copy of the data structure maintained by the handed in
-     * array instance.
-     */
-    SparseArray(const SparseArray& other)
-            : unsynced(RootInfo{clone(other.unsynced.root, other.unsynced.levels), other.unsynced.levels,
-                      other.unsynced.offset, nullptr, other.unsynced.firstOffset}) {
-        if (unsynced.root) {
-            unsynced.root->parent = nullptr;
-            unsynced.first = findFirst(unsynced.root, unsynced.levels);
-        }
-    }
-
-    /**
-     * A r-value based copy constructor for sparse arrays. It
-     * takes over ownership of the structure maintained by the
-     * handed in array.
-     */
-    SparseArray(SparseArray&& other)
-            : unsynced(RootInfo{other.unsynced.root, other.unsynced.levels, other.unsynced.offset,
-                      other.unsynced.first, other.unsynced.firstOffset}) {
-        other.unsynced.root = nullptr;
-        other.unsynced.levels = 0;
-        other.unsynced.first = nullptr;
-    }
-
-    /**
-     * A destructor for sparse arrays clearing up the internally
-     * maintained data structure.
-     */
-    ~SparseArray() {
-        clean();
-    }
-
-    /**
-     * An assignment creating a deep copy of the handed in
-     * array structure (utilizing the copy functor provided
-     * as a template parameter).
-     */
-    SparseArray& operator=(const SparseArray& other) {
-        if (this == &other) return *this;
-
-        // clean this one
-        clean();
-
-        // copy content
-        unsynced.levels = other.unsynced.levels;
-        unsynced.root = clone(other.unsynced.root, unsynced.levels);
-        if (unsynced.root) {
-            unsynced.root->parent = nullptr;
-        }
-        unsynced.offset = other.unsynced.offset;
-        unsynced.first = (unsynced.root) ? findFirst(unsynced.root, unsynced.levels) : nullptr;
-        unsynced.firstOffset = other.unsynced.firstOffset;
-
-        // done
-        return *this;
-    }
-
-    /**
-     * An assignment operation taking over ownership
-     * from a r-value reference to a sparse array.
-     */
-    SparseArray& operator=(SparseArray&& other) {
-        // clean this one
-        clean();
-
-        // harvest content
-        unsynced.root = other.unsynced.root;
-        unsynced.levels = other.unsynced.levels;
-        unsynced.offset = other.unsynced.offset;
-        unsynced.first = other.unsynced.first;
-        unsynced.firstOffset = other.unsynced.firstOffset;
-
-        // reset other
-        other.unsynced.root = nullptr;
-        other.unsynced.levels = 0;
-        other.unsynced.first = nullptr;
-
-        // done
-        return *this;
-    }
-
-    /**
-     * Tests whether this sparse array is empty, thus it only
-     * contains default-values, or not.
-     */
-    bool empty() const {
-        return unsynced.root == nullptr;
-    }
-
-    /**
-     * Computes the number of non-empty elements within this
-     * sparse array.
-     */
-    std::size_t size() const {
-        // quick one for the empty map
-        if (empty()) return 0;
-
-        // count elements -- since maintaining is making inserts more expensive
-        std::size_t res = 0;
-        for (auto it = begin(); it != end(); ++it) {
-            ++res;
-        }
-        return res;
-    }
-
-private:
-    /**
-     * Computes the memory usage of the given sub-tree.
-     */
-    static std::size_t getMemoryUsage(const Node* node, int level) {
-        // support null-nodes
-        if (!node) return 0;
-
-        // add size of current node
-        std::size_t res = sizeof(Node);
-
-        // sum up memory usage of child nodes
-        if (level > 0) {
-            for (int i = 0; i < NUM_CELLS; i++) {
-                res += getMemoryUsage(node->cell[i].ptr, level - 1);
-            }
-        }
-
-        // done
-        return res;
-    }
-
-public:
-    /**
-     * Computes the total memory usage of this data structure.
-     */
-    std::size_t getMemoryUsage() const {
-        // the memory of the wrapper class
-        std::size_t res = sizeof(*this);
-
-        // add nodes
-        if (unsynced.root) {
-            res += getMemoryUsage(unsynced.root, unsynced.levels);
-        }
-
-        // done
-        return res;
-    }
-
-    /**
-     * Resets the content of this array to default values for each contained
-     * element.
-     */
-    void clear() {
-        clean();
-        unsynced.root = nullptr;
-        unsynced.levels = 0;
-        unsynced.first = nullptr;
-        unsynced.firstOffset = std::numeric_limits<index_type>::max();
-    }
-
-    /**
-     * A struct to be utilized as a local, temporal context by client code
-     * to speed up the execution of various operations (optional parameter).
-     */
-    struct op_context {
-        index_type lastIndex{0};
-        Node* lastNode{nullptr};
-        op_context() = default;
-    };
-
-private:
-    // ---------------------------------------------------------------------
-    //              Optimistic Locking of Root-Level Infos
-    // ---------------------------------------------------------------------
-
-    /**
-     * A struct to cover a snapshot of the root node state.
-     */
-    struct RootInfoSnapshot {
-        // the current pointer to a root node
-        Node* root;
-        // the current number of levels
-        uint32_t levels;
-        // the current offset of the first theoretical element
-        index_type offset;
-        // a version number for the optimistic locking
-        uintptr_t version;
-    };
-
-    /**
-     * Obtains the current version of the root.
-     */
-    uint64_t getRootVersion() const {
-        // here it is assumed that the load of a 64-bit word is atomic
-        return (uint64_t)synced.root;
-    }
-
-    /**
-     * Obtains a snapshot of the current root information.
-     */
-    RootInfoSnapshot getRootInfo() const {
-        RootInfoSnapshot res{};
-        do {
-            // first take the mod counter
-            do {
-                // if res.mod % 2 == 1 .. there is an update in progress
-                res.version = getRootVersion();
-            } while (res.version % 2);
-
-            // then the rest
-            res.root = synced.root;
-            res.levels = synced.levels;
-            res.offset = synced.offset;
-
-            // check consistency of obtained data (optimistic locking)
-        } while (res.version != getRootVersion());
-
-        // got a consistent snapshot
-        return res;
-    }
-
-    /**
-     * Updates the current root information based on the handed in modified
-     * snapshot instance if the version number of the snapshot still corresponds
-     * to the current version. Otherwise a concurrent update took place and the
-     * operation is aborted.
-     *
-     * @param info the updated information to be assigned to the active root-info data
-     * @return true if successfully updated, false if aborted
-     */
-    bool tryUpdateRootInfo(const RootInfoSnapshot& info) {
-        // check mod counter
-        uintptr_t version = info.version;
-
-        // update root to invalid pointer (ending with 1)
-        if (!__sync_bool_compare_and_swap(&synced.root, (Node*)version, (Node*)(version + 1))) {
-            return false;
-        }
-
-        // conduct update
-        synced.levels = info.levels;
-        synced.offset = info.offset;
-
-        // update root (and thus the version to enable future retrievals)
-        __sync_synchronize();
-        synced.root = info.root;
-
-        // done
-        return true;
-    }
-
-    /**
-     * A struct summarizing the state of the first node reference.
-     */
-    struct FirstInfoSnapshot {
-        // the pointer to the first node
-        Node* node;
-        // the offset of the first node
-        index_type offset;
-        // the version number of the first node (for the optimistic locking)
-        uintptr_t version;
-    };
-
-    /**
-     * Obtains the current version number of the first node information.
-     */
-    uint64_t getFirstVersion() const {
-        // here it is assumed that the load of a 64-bit word is atomic
-        return (uint64_t)synced.first;
-    }
-
-    /**
-     * Obtains a snapshot of the current first-node information.
-     */
-    FirstInfoSnapshot getFirstInfo() const {
-        FirstInfoSnapshot res{};
-        do {
-            // first take the version
-            do {
-                res.version = getFirstVersion();
-            } while (res.version % 2);
-
-            // collect the values
-            res.node = synced.first;
-            res.offset = synced.firstOffset;
-
-        } while (res.version != getFirstVersion());
-
-        // we got a consistent snapshot
-        return res;
-    }
-
-    /**
-     * Updates the information stored regarding the first node in a
-     * concurrent setting utilizing a optimistic locking approach.
-     * This is identical to the approach utilized for the root info.
-     */
-    bool tryUpdateFirstInfo(const FirstInfoSnapshot& info) {
-        // check mod counter
-        uintptr_t version = info.version;
-
-        // temporary update first pointer to point to uneven value (lock-out)
-        if (!__sync_bool_compare_and_swap(&synced.first, (Node*)version, (Node*)(version + 1))) {
-            return false;
-        }
-
-        // conduct update
-        synced.firstOffset = info.offset;
-
-        // update node pointer (and thus the version number)
-        __sync_synchronize();
-        synced.first = info.node;  // must be last (and atomic)
-
-        // done
-        return true;
-    }
-
-public:
-    /**
-     * Obtains a mutable reference to the value addressed by the given index.
-     *
-     * @param i the index of the element to be addressed
-     * @return a mutable reference to the corresponding element
-     */
-    value_type& get(index_type i) {
-        op_context ctxt;
-        return get(i, ctxt);
-    }
-
-    /**
-     * Obtains a mutable reference to the value addressed by the given index.
-     *
-     * @param i the index of the element to be addressed
-     * @param ctxt a operation context to exploit state-less temporal locality
-     * @return a mutable reference to the corresponding element
-     */
-    value_type& get(index_type i, op_context& ctxt) {
-        return getLeaf(i, ctxt).value;
-    }
-
-    /**
-     * Obtains a mutable reference to the atomic value addressed by the given index.
-     *
-     * @param i the index of the element to be addressed
-     * @return a mutable reference to the corresponding element
-     */
-    atomic_value_type& getAtomic(index_type i) {
-        op_context ctxt;
-        return getAtomic(i, ctxt);
-    }
-
-    /**
-     * Obtains a mutable reference to the atomic value addressed by the given index.
-     *
-     * @param i the index of the element to be addressed
-     * @param ctxt a operation context to exploit state-less temporal locality
-     * @return a mutable reference to the corresponding element
-     */
-    atomic_value_type& getAtomic(index_type i, op_context& ctxt) {
-        return getLeaf(i, ctxt).avalue;
-    }
-
-private:
-    /**
-     * An internal function capable of navigating to a given leaf node entry.
-     * If the cell does not exist yet it will be created as a side-effect.
-     *
-     * @param i the index of the requested cell
-     * @param ctxt a operation context to exploit state-less temporal locality
-     * @return a reference to the requested cell
-     */
-    inline Cell& getLeaf(index_type i, op_context& ctxt) {
-        // check context
-        if (ctxt.lastNode && (ctxt.lastIndex == (i & ~INDEX_MASK))) {
-            // return reference to referenced
-            return ctxt.lastNode->cell[i & INDEX_MASK];
-        }
-
-        // get snapshot of root
-        auto info = getRootInfo();
-
-        // check for emptiness
-        if (info.root == nullptr) {
-            // build new root node
-            info.root = newNode();
-
-            // initialize the new node
-            info.root->parent = nullptr;
-            info.offset = i & ~(INDEX_MASK);
-
-            // try updating root information atomically
-            if (tryUpdateRootInfo(info)) {
-                // success -- finish get call
-
-                // update first
-                auto firstInfo = getFirstInfo();
-                while (info.offset < firstInfo.offset) {
-                    firstInfo.node = info.root;
-                    firstInfo.offset = info.offset;
-                    if (!tryUpdateFirstInfo(firstInfo)) {
-                        // there was some concurrent update => check again
-                        firstInfo = getFirstInfo();
-                    }
-                }
-
-                // return reference to proper cell
-                return info.root->cell[i & INDEX_MASK];
-            }
-
-            // somebody else was faster => use standard insertion procedure
-            delete info.root;
-
-            // retrieve new root info
-            info = getRootInfo();
-
-            // make sure there is a root
-            assert(info.root);
-        }
-
-        // for all other inserts
-        //   - check boundary
-        //   - navigate to node
-        //   - insert value
-
-        // check boundaries
-        while (!inBoundaries(i, info.levels, info.offset)) {
-            // boundaries need to be expanded by growing upwards
-            raiseLevel(info);  // try raising level unless someone else did already
-            // update root info
-            info = getRootInfo();
-        }
-
-        // navigate to node
-        Node* node = info.root;
-        unsigned level = info.levels;
-        while (level != 0) {
-            // get X coordinate
-            auto x = getIndex(static_cast<RamDomain>(i), level);
-
-            // decrease level counter
-            --level;
-
-            // check next node
-            std::atomic<Node*>& aNext = node->cell[x].aptr;
-            Node* next = aNext;
-            if (!next) {
-                // create new sub-tree
-                Node* newNext = newNode();
-                newNext->parent = node;
-
-                // try to update next
-                if (!aNext.compare_exchange_strong(next, newNext)) {
-                    // some other thread was faster => use updated next
-                    delete newNext;
-                } else {
-                    // the locally created next is the new next
-                    next = newNext;
-
-                    // update first
-                    if (level == 0) {
-                        // compute offset of this node
-                        auto off = i & ~INDEX_MASK;
-
-                        // fast over-approximation of whether a update is necessary
-                        if (off < unsynced.firstOffset) {
-                            // update first reference if this one is the smallest
-                            auto first_info = getFirstInfo();
-                            while (off < first_info.offset) {
-                                first_info.node = next;
-                                first_info.offset = off;
-                                if (!tryUpdateFirstInfo(first_info)) {
-                                    // there was some concurrent update => check again
-                                    first_info = getFirstInfo();
-                                }
-                            }
-                        }
-                    }
-                }
-
-                // now next should be defined
-                assert(next);
-            }
-
-            // continue one level below
-            node = next;
-        }
-
-        // update context
-        ctxt.lastIndex = (i & ~INDEX_MASK);
-        ctxt.lastNode = node;
-
-        // return reference to cell
-        return node->cell[i & INDEX_MASK];
-    }
-
-public:
-    /**
-     * Updates the value stored in cell i by the given value.
-     */
-    void update(index_type i, const value_type& val) {
-        op_context ctxt;
-        update(i, val, ctxt);
-    }
-
-    /**
-     * Updates the value stored in cell i by the given value. A operation
-     * context can be provided for exploiting temporal locality.
-     */
-    void update(index_type i, const value_type& val, op_context& ctxt) {
-        get(i, ctxt) = val;
-    }
-
-    /**
-     * Obtains the value associated to index i -- which might be
-     * the default value of the covered type if the value hasn't been
-     * defined previously.
-     */
-    value_type operator[](index_type i) const {
-        return lookup(i);
-    }
-
-    /**
-     * Obtains the value associated to index i -- which might be
-     * the default value of the covered type if the value hasn't been
-     * defined previously.
-     */
-    value_type lookup(index_type i) const {
-        op_context ctxt;
-        return lookup(i, ctxt);
-    }
-
-    /**
-     * Obtains the value associated to index i -- which might be
-     * the default value of the covered type if the value hasn't been
-     * defined previously. A operation context can be provided for
-     * exploiting temporal locality.
-     */
-    value_type lookup(index_type i, op_context& ctxt) const {
-        // check whether it is empty
-        if (!unsynced.root) return souffle::detail::default_factory<value_type>()();
-
-        // check boundaries
-        if (!inBoundaries(i)) return souffle::detail::default_factory<value_type>()();
-
-        // check context
-        if (ctxt.lastNode && ctxt.lastIndex == (i & ~INDEX_MASK)) {
-            return ctxt.lastNode->cell[i & INDEX_MASK].value;
-        }
-
-        // navigate to value
-        Node* node = unsynced.root;
-        unsigned level = unsynced.levels;
-        while (level != 0) {
-            // get X coordinate
-            auto x = getIndex(static_cast<RamDomain>(i), level);
-
-            // decrease level counter
-            --level;
-
-            // check next node
-            Node* next = node->cell[x].ptr;
-
-            // check next step
-            if (!next) return souffle::detail::default_factory<value_type>()();
-
-            // continue one level below
-            node = next;
-        }
-
-        // remember context
-        ctxt.lastIndex = (i & ~INDEX_MASK);
-        ctxt.lastNode = node;
-
-        // return reference to cell
-        return node->cell[i & INDEX_MASK].value;
-    }
-
-private:
-    /**
-     * A static operation utilized internally for merging sub-trees recursively.
-     *
-     * @param parent the parent node of the current merge operation
-     * @param trg a reference to the pointer the cloned node should be stored to
-     * @param src the node to be cloned
-     * @param levels the height of the cloned node
-     */
-    static void merge(const Node* parent, Node*& trg, const Node* src, int levels) {
-        // if other side is null => done
-        if (src == nullptr) {
-            return;
-        }
-
-        // if the trg sub-tree is empty, clone the corresponding branch
-        if (trg == nullptr) {
-            trg = clone(src, levels);
-            if (trg != nullptr) {
-                trg->parent = parent;
-            }
-            return;  // done
-        }
-
-        // otherwise merge recursively
-
-        // the leaf-node step
-        if (levels == 0) {
-            merge_op merg;
-            for (int i = 0; i < NUM_CELLS; ++i) {
-                trg->cell[i].value = merg(trg->cell[i].value, src->cell[i].value);
-            }
-            return;
-        }
-
-        // the recursive step
-        for (int i = 0; i < NUM_CELLS; ++i) {
-            merge(trg, trg->cell[i].ptr, src->cell[i].ptr, levels - 1);
-        }
-    }
-
-public:
-    /**
-     * Adds all the values stored in the given array to this array.
-     */
-    void addAll(const SparseArray& other) {
-        // skip if other is empty
-        if (other.empty()) {
-            return;
-        }
-
-        // special case: emptiness
-        if (empty()) {
-            // use assignment operator
-            *this = other;
-            return;
-        }
-
-        // adjust levels
-        while (unsynced.levels < other.unsynced.levels || !inBoundaries(other.unsynced.offset)) {
-            raiseLevel();
-        }
-
-        // navigate to root node equivalent of the other node in this tree
-        auto level = unsynced.levels;
-        Node** node = &unsynced.root;
-        while (level > other.unsynced.levels) {
-            // get X coordinate
-            auto x = getIndex(static_cast<RamDomain>(other.unsynced.offset), level);
-
-            // decrease level counter
-            --level;
-
-            // check next node
-            Node*& next = (*node)->cell[x].ptr;
-            if (!next) {
-                // create new sub-tree
-                next = newNode();
-                next->parent = *node;
-            }
-
-            // continue one level below
-            node = &next;
-        }
-
-        // merge sub-branches from here
-        merge((*node)->parent, *node, other.unsynced.root, level);
-
-        // update first
-        if (unsynced.firstOffset > other.unsynced.firstOffset) {
-            unsynced.first = findFirst(*node, level);
-            unsynced.firstOffset = other.unsynced.firstOffset;
-        }
-    }
-
-    // ---------------------------------------------------------------------
-    //                           Iterator
-    // ---------------------------------------------------------------------
-
-    /**
-     * The iterator type to be utilized to iterate over the non-default elements of this array.
-     */
-    class iterator {
-        using pair_type = std::pair<index_type, value_type>;
-
-        // a pointer to the leaf node currently processed or null (end)
-        const Node* node;
-
-        // the value currently pointed to
-        pair_type value;
-
-    public:
-        // default constructor -- creating an end-iterator
-        iterator() : node(nullptr) {}
-
-        iterator(const Node* node, pair_type value) : node(node), value(std::move(value)) {}
-
-        iterator(const Node* first, index_type firstOffset) : node(first), value(firstOffset, 0) {
-            // if the start is the end => we are done
-            if (!first) return;
-
-            // load the value
-            if (first->cell[0].value == value_type()) {
-                ++(*this);  // walk to first element
-            } else {
-                value.second = first->cell[0].value;
-            }
-        }
-
-        // a copy constructor
-        iterator(const iterator& other) = default;
-
-        // an assignment operator
-        iterator& operator=(const iterator& other) = default;
-
-        // the equality operator as required by the iterator concept
-        bool operator==(const iterator& other) const {
-            // only equivalent if pointing to the end
-            return (node == nullptr && other.node == nullptr) ||
-                   (node == other.node && value.first == other.value.first);
-        }
-
-        // the not-equality operator as required by the iterator concept
-        bool operator!=(const iterator& other) const {
-            return !(*this == other);
-        }
-
-        // the deref operator as required by the iterator concept
-        const pair_type& operator*() const {
-            return value;
-        }
-
-        // support for the pointer operator
-        const pair_type* operator->() const {
-            return &value;
-        }
-
-        // the increment operator as required by the iterator concept
-        iterator& operator++() {
-            // get current offset
-            index_type x = value.first & INDEX_MASK;
-
-            // go to next non-empty value in current node
-            do {
-                x++;
-            } while (x < NUM_CELLS && node->cell[x].value == value_type());
-
-            // check whether one has been found
-            if (x < NUM_CELLS) {
-                // update value and be done
-                value.first = (value.first & ~INDEX_MASK) | x;
-                value.second = node->cell[x].value;
-                return *this;  // done
-            }
-
-            // go to parent
-            node = node->parent;
-            int level = 1;
-
-            // get current index on this level
-            x = getIndex(static_cast<RamDomain>(value.first), level);
-            x++;
-
-            while (level > 0 && node) {
-                // search for next child
-                while (x < NUM_CELLS) {
-                    if (node->cell[x].ptr != nullptr) {
-                        break;
-                    }
-                    x++;
-                }
-
-                // pick next step
-                if (x < NUM_CELLS) {
-                    // going down
-                    node = node->cell[x].ptr;
-                    value.first &= getLevelMask(level + 1);
-                    value.first |= x << (BIT_PER_STEP * level);
-                    level--;
-                    x = 0;
-                } else {
-                    // going up
-                    node = node->parent;
-                    level++;
-
-                    // get current index on this level
-                    x = getIndex(static_cast<RamDomain>(value.first), level);
-                    x++;  // go one step further
-                }
-            }
-
-            // check whether it is the end of range
-            if (node == nullptr) {
-                return *this;
-            }
-
-            // search the first value in this node
-            x = 0;
-            while (node->cell[x].value == value_type()) {
-                x++;
-            }
-
-            // update value
-            value.first |= x;
-            value.second = node->cell[x].value;
-
-            // done
-            return *this;
-        }
-
-        // True if this iterator is passed the last element.
-        bool isEnd() const {
-            return node == nullptr;
-        }
-
-        // enables this iterator core to be printed (for debugging)
-        void print(std::ostream& out) const {
-            out << "SparseArrayIter(" << node << " @ " << value << ")";
-        }
-
-        friend std::ostream& operator<<(std::ostream& out, const iterator& iter) {
-            iter.print(out);
-            return out;
-        }
-    };
-
-    /**
-     * Obtains an iterator referencing the first non-default element or end in
-     * case there are no such elements.
-     */
-    iterator begin() const {
-        return iterator(unsynced.first, unsynced.firstOffset);
-    }
-
-    /**
-     * An iterator referencing the position after the last non-default element.
-     */
-    iterator end() const {
-        return iterator();
-    }
-
-    /**
-     * An operation to obtain an iterator referencing an element addressed by the
-     * given index. If the corresponding element is a non-default value, a corresponding
-     * iterator will be returned. Otherwise end() will be returned.
-     */
-    iterator find(index_type i) const {
-        op_context ctxt;
-        return find(i, ctxt);
-    }
-
-    /**
-     * An operation to obtain an iterator referencing an element addressed by the
-     * given index. If the corresponding element is a non-default value, a corresponding
-     * iterator will be returned. Otherwise end() will be returned. A operation context
-     * can be provided for exploiting temporal locality.
-     */
-    iterator find(index_type i, op_context& ctxt) const {
-        // check whether it is empty
-        if (!unsynced.root) return end();
-
-        // check boundaries
-        if (!inBoundaries(i)) return end();
-
-        // check context
-        if (ctxt.lastNode && ctxt.lastIndex == (i & ~INDEX_MASK)) {
-            Node* node = ctxt.lastNode;
-
-            // check whether there is a proper entry
-            value_type value = node->cell[i & INDEX_MASK].value;
-            if (value == value_type{}) {
-                return end();
-            }
-            // return iterator pointing to value
-            return iterator(node, std::make_pair(i, value));
-        }
-
-        // navigate to value
-        Node* node = unsynced.root;
-        unsigned level = unsynced.levels;
-        while (level != 0) {
-            // get X coordinate
-            auto x = getIndex(i, level);
-
-            // decrease level counter
-            --level;
-
-            // check next node
-            Node* next = node->cell[x].ptr;
-
-            // check next step
-            if (!next) return end();
-
-            // continue one level below
-            node = next;
-        }
-
-        // register in context
-        ctxt.lastNode = node;
-        ctxt.lastIndex = (i & ~INDEX_MASK);
-
-        // check whether there is a proper entry
-        value_type value = node->cell[i & INDEX_MASK].value;
-        if (value == value_type{}) {
-            return end();
-        }
-
-        // return iterator pointing to cell
-        return iterator(node, std::make_pair(i, value));
-    }
-
-    /**
-     * An operation obtaining the smallest non-default element such that it's index is >=
-     * the given index.
-     */
-    iterator lowerBound(index_type i) const {
-        op_context ctxt;
-        return lowerBound(i, ctxt);
-    }
-
-    /**
-     * An operation obtaining the smallest non-default element such that it's index is >=
-     * the given index. A operation context can be provided for exploiting temporal locality.
-     */
-    iterator lowerBound(index_type i, op_context&) const {
-        // check whether it is empty
-        if (!unsynced.root) return end();
-
-        // check boundaries
-        if (!inBoundaries(i)) {
-            // if it is on the lower end, return minimum result
-            if (i < unsynced.offset) {
-                const auto& value = unsynced.first->cell[0].value;
-                auto res = iterator(unsynced.first, std::make_pair(unsynced.offset, value));
-                if (value == value_type()) {
-                    ++res;
-                }
-                return res;
-            }
-            // otherwise it is on the high end, return end iterator
-            return end();
-        }
-
-        // navigate to value
-        Node* node = unsynced.root;
-        unsigned level = unsynced.levels;
-        while (true) {
-            // get X coordinate
-            auto x = getIndex(static_cast<RamDomain>(i), level);
-
-            // check next node
-            Node* next = node->cell[x].ptr;
-
-            // check next step
-            if (!next) {
-                if (x == NUM_CELLS - 1) {
-                    ++level;
-                    node = const_cast<Node*>(node->parent);
-                    if (!node) return end();
-                }
-
-                // continue search
-                i = i & getLevelMask(level);
-
-                // find next higher value
-                i += 1ull << (BITS * level);
-
-            } else {
-                if (level == 0) {
-                    // found boundary
-                    return iterator(node, std::make_pair(i, node->cell[x].value));
-                }
-
-                // decrease level counter
-                --level;
-
-                // continue one level below
-                node = next;
-            }
-        }
-    }
-
-    /**
-     * An operation obtaining the smallest non-default element such that it's index is greater
-     * the given index.
-     */
-    iterator upperBound(index_type i) const {
-        op_context ctxt;
-        return upperBound(i, ctxt);
-    }
-
-    /**
-     * An operation obtaining the smallest non-default element such that it's index is greater
-     * the given index. A operation context can be provided for exploiting temporal locality.
-     */
-    iterator upperBound(index_type i, op_context& ctxt) const {
-        if (i == std::numeric_limits<index_type>::max()) {
-            return end();
-        }
-        return lowerBound(i + 1, ctxt);
-    }
-
-private:
-    /**
-     * An internal debug utility printing the internal structure of this sparse array to the given output
-     * stream.
-     */
-    void dump(bool detailed, std::ostream& out, const Node& node, int level, index_type offset,
-            int indent = 0) const {
-        auto x = getIndex(offset, level + 1);
-        out << times("\t", indent) << x << ": Node " << &node << " on level " << level
-            << " parent: " << node.parent << " -- range: " << offset << " - "
-            << (offset + ~getLevelMask(level + 1)) << "\n";
-
-        if (level == 0) {
-            for (int i = 0; i < NUM_CELLS; i++) {
-                if (detailed || node.cell[i].value != value_type()) {
-                    out << times("\t", indent + 1) << i << ": [" << (offset + i) << "] " << node.cell[i].value
-                        << "\n";
-                }
-            }
-        } else {
-            for (int i = 0; i < NUM_CELLS; i++) {
-                if (node.cell[i].ptr) {
-                    dump(detailed, out, *node.cell[i].ptr, level - 1,
-                            offset + (i * (index_type(1) << (level * BIT_PER_STEP))), indent + 1);
-                } else if (detailed) {
-                    auto low = offset + (i * (1 << (level * BIT_PER_STEP)));
-                    auto hig = low + ~getLevelMask(level);
-                    out << times("\t", indent + 1) << i << ": empty range " << low << " - " << hig << "\n";
-                }
-            }
-        }
-        out << "\n";
-    }
-
-public:
-    /**
-     * A debug utility printing the internal structure of this sparse array to the given output stream.
-     */
-    void dump(bool detail = false, std::ostream& out = std::cout) const {
-        if (!unsynced.root) {
-            out << " - empty - \n";
-            return;
-        }
-        out << "root:  " << unsynced.root << "\n";
-        out << "offset: " << unsynced.offset << "\n";
-        out << "first: " << unsynced.first << "\n";
-        out << "fist offset: " << unsynced.firstOffset << "\n";
-        dump(detail, out, *unsynced.root, unsynced.levels, unsynced.offset);
-    }
-
-private:
-    // --------------------------------------------------------------------------
-    //                                 Utilities
-    // --------------------------------------------------------------------------
-
-    /**
-     * Creates new nodes and initializes them with 0.
-     */
-    static Node* newNode() {
-        return new Node();
-    }
-
-    /**
-     * Destroys a node and all its sub-nodes recursively.
-     */
-    static void freeNodes(Node* node, int level) {
-        if (!node) return;
-        if (level != 0) {
-            for (int i = 0; i < NUM_CELLS; i++) {
-                freeNodes(node->cell[i].ptr, level - 1);
-            }
-        }
-        delete node;
-    }
-
-    /**
-     * Conducts a cleanup of the internal tree structure.
-     */
-    void clean() {
-        freeNodes(unsynced.root, unsynced.levels);
-        unsynced.root = nullptr;
-        unsynced.levels = 0;
-    }
-
-    /**
-     * Clones the given node and all its sub-nodes.
-     */
-    static Node* clone(const Node* node, int level) {
-        // support null-pointers
-        if (node == nullptr) {
-            return nullptr;
-        }
-
-        // create a clone
-        auto* res = new Node();
-
-        // handle leaf level
-        if (level == 0) {
-            copy_op copy;
-            for (int i = 0; i < NUM_CELLS; i++) {
-                res->cell[i].value = copy(node->cell[i].value);
-            }
-            return res;
-        }
-
-        // for inner nodes clone each child
-        for (int i = 0; i < NUM_CELLS; i++) {
-            auto cur = clone(node->cell[i].ptr, level - 1);
-            if (cur != nullptr) {
-                cur->parent = res;
-            }
-            res->cell[i].ptr = cur;
-        }
-
-        // done
-        return res;
-    }
-
-    /**
-     * Obtains the left-most leaf-node of the tree rooted by the given node
-     * with the given level.
-     */
-    static Node* findFirst(Node* node, int level) {
-        while (level > 0) {
-            bool found = false;
-            for (int i = 0; i < NUM_CELLS; i++) {
-                Node* cur = node->cell[i].ptr;
-                if (cur) {
-                    node = cur;
-                    --level;
-                    found = true;
-                    break;
-                }
-            }
-            assert(found && "No first node!");
-        }
-
-        return node;
-    }
-
-    /**
-     * Raises the level of this tree by one level. It does so by introducing
-     * a new root node and inserting the current root node as a child node.
-     */
-    void raiseLevel() {
-        // something went wrong when we pass that line
-        assert(unsynced.levels < (sizeof(index_type) * 8 / BITS) + 1);
-
-        // create new root
-        Node* node = newNode();
-        node->parent = nullptr;
-
-        // insert existing root as child
-        auto x = getIndex(static_cast<RamDomain>(unsynced.offset), unsynced.levels + 1);
-        node->cell[x].ptr = unsynced.root;
-
-        // swap the root
-        unsynced.root->parent = node;
-
-        // update root
-        unsynced.root = node;
-        ++unsynced.levels;
-
-        // update offset be removing additional bits
-        unsynced.offset &= getLevelMask(unsynced.levels + 1);
-    }
-
-    /**
-     * Attempts to raise the height of this tree based on the given root node
-     * information and updates the root-info snapshot correspondingly.
-     */
-    void raiseLevel(RootInfoSnapshot& info) {
-        // something went wrong when we pass that line
-        assert(info.levels < (sizeof(index_type) * 8 / BITS) + 1);
-
-        // create new root
-        Node* newRoot = newNode();
-        newRoot->parent = nullptr;
-
-        // insert existing root as child
-        auto x = getIndex(static_cast<RamDomain>(info.offset), info.levels + 1);
-        newRoot->cell[x].ptr = info.root;
-
-        // exchange the root in the info struct
-        auto oldRoot = info.root;
-        info.root = newRoot;
-
-        // update level counter
-        ++info.levels;
-
-        // update offset
-        info.offset &= getLevelMask(info.levels + 1);
-
-        // try exchanging root info
-        if (tryUpdateRootInfo(info)) {
-            // success => final step, update parent of old root
-            oldRoot->parent = info.root;
-        } else {
-            // throw away temporary new node
-            delete newRoot;
-        }
-    }
-
-    /**
-     * Tests whether the given index is covered by the boundaries defined
-     * by the hight and offset of the internally maintained tree.
-     */
-    bool inBoundaries(index_type a) const {
-        return inBoundaries(a, unsynced.levels, unsynced.offset);
-    }
-
-    /**
-     * Tests whether the given index is within the boundaries defined by the
-     * given tree hight and offset.
-     */
-    static bool inBoundaries(index_type a, uint32_t levels, index_type offset) {
-        auto mask = getLevelMask(levels + 1);
-        return (a & mask) == offset;
-    }
-
-    /**
-     * Obtains the index within the arrays of cells of a given index on a given
-     * level of the internally maintained tree.
-     */
-    static index_type getIndex(RamDomain a, unsigned level) {
-        return (a & (INDEX_MASK << (level * BIT_PER_STEP))) >> (level * BIT_PER_STEP);
-    }
-
-    /**
-     * Computes the bit-mask to be applicable to obtain the offset of a node on a
-     * given tree level.
-     */
-    static index_type getLevelMask(unsigned level) {
-        if (level > (sizeof(index_type) * 8 / BITS)) return 0;
-        return (~(index_type(0)) << (level * BIT_PER_STEP));
-    }
-};
-
-/**
- * A sparse bit-map is a bit map virtually assigning a bit value to every value if the
- * uint32_t domain. However, only 1-bits are stored utilizing a nested sparse array
- * structure.
- *
- * @tparam BITS similar to the BITS parameter of the sparse array type
- */
-template <unsigned BITS = 4>
-class SparseBitMap {
-    // the element type stored in the nested sparse array
-    using value_t = uint64_t;
-
-    // define the bit-level merge operation
-    struct merge_op {
-        value_t operator()(value_t a, value_t b) const {
-            return a | b;  // merging bit masks => bitwise or operation
-        }
-    };
-
-    // the type of the internal data store
-    using data_store_t = SparseArray<value_t, BITS, merge_op>;
-    using atomic_value_t = typename data_store_t::atomic_value_type;
-
-    // some constants for manipulating stored values
-    static constexpr short BITS_PER_ENTRY = sizeof(value_t) * 8;
-    static constexpr short LEAF_INDEX_WIDTH = static_cast<short>(__builtin_ctz(BITS_PER_ENTRY));
-    static constexpr uint64_t LEAF_INDEX_MASK = BITS_PER_ENTRY - 1;
-
-public:
-    // the type to address individual entries
-    using index_type = typename data_store_t::index_type;
-
-private:
-    // it utilizes a sparse map to store its data
-    data_store_t store;
-
-public:
-    // a simple default constructor
-    SparseBitMap() = default;
-
-    // a default copy constructor
-    SparseBitMap(const SparseBitMap&) = default;
-
-    // a default r-value copy constructor
-    SparseBitMap(SparseBitMap&&) = default;
-
-    // a default assignment operator
-    SparseBitMap& operator=(const SparseBitMap&) = default;
-
-    // a default r-value assignment operator
-    SparseBitMap& operator=(SparseBitMap&&) = default;
-
-    // checks whether this bit-map is empty -- thus it does not have any 1-entries
-    bool empty() const {
-        return store.empty();
-    }
-
-    // the type utilized for recording context information for exploiting temporal locality
-    using op_context = typename data_store_t::op_context;
-
-    /**
-     * Sets the bit addressed by i to 1.
-     */
-    bool set(index_type i) {
-        op_context ctxt;
-        return set(i, ctxt);
-    }
-
-    /**
-     * Sets the bit addressed by i to 1. A context for exploiting temporal locality
-     * can be provided.
-     */
-    bool set(index_type i, op_context& ctxt) {
-        atomic_value_t& val = store.getAtomic(i >> LEAF_INDEX_WIDTH, ctxt);
-        value_t bit = (1ull << (i & LEAF_INDEX_MASK));
-
-#ifdef __GNUC__
-#if __GNUC__ >= 7
-        // In GCC >= 7 the usage of fetch_or causes a bug that needs further investigation
-        // For now, this two-instruction based implementation provides a fix that does
-        // not sacrifice too much performance.
-
-        while (true) {
-            auto order = std::memory_order::memory_order_relaxed;
-
-            // load current value
-            value_t old = val.load(order);
-
-            // if bit is already set => we are done
-            if (old & bit) return false;
-
-            // set the bit, if failed, repeat
-            if (!val.compare_exchange_strong(old, old | bit, order, order)) continue;
-
-            // it worked, new bit added
-            return true;
-        }
-
-#endif
-#endif
-
-        value_t old = val.fetch_or(bit, std::memory_order::memory_order_relaxed);
-        return (old & bit) == 0u;
-    }
-
-    /**
-     * Determines the whether the bit addressed by i is set or not.
-     */
-    bool test(index_type i) const {
-        op_context ctxt;
-        return test(i, ctxt);
-    }
-
-    /**
-     * Determines the whether the bit addressed by i is set or not. A context for
-     * exploiting temporal locality can be provided.
-     */
-    bool test(index_type i, op_context& ctxt) const {
-        value_t bit = (1ull << (i & LEAF_INDEX_MASK));
-        return store.lookup(i >> LEAF_INDEX_WIDTH, ctxt) & bit;
-    }
-
-    /**
-     * Determines the whether the bit addressed by i is set or not.
-     */
-    bool operator[](index_type i) const {
-        return test(i);
-    }
-
-    /**
-     * Resets all contained bits to 0.
-     */
-    void clear() {
-        store.clear();
-    }
-
-    /**
-     * Determines the number of bits set.
-     */
-    std::size_t size() const {
-        // this is computed on demand to keep the set operation simple.
-        std::size_t res = 0;
-        for (const auto& cur : store) {
-            res += __builtin_popcountll(cur.second);
-        }
-        return res;
-    }
-
-    /**
-     * Computes the total memory usage of this data structure.
-     */
-    std::size_t getMemoryUsage() const {
-        // compute the total memory usage
-        return sizeof(*this) - sizeof(data_store_t) + store.getMemoryUsage();
-    }
-
-    /**
-     * Sets all bits set in other to 1 within this bit map.
-     */
-    void addAll(const SparseBitMap& other) {
-        // nothing to do if it is a self-assignment
-        if (this == &other) return;
-
-        // merge the sparse store
-        store.addAll(other.store);
-    }
-
-    // ---------------------------------------------------------------------
-    //                           Iterator
-    // ---------------------------------------------------------------------
-
-    /**
-     * An iterator iterating over all indices set to 1.
-     */
-    class iterator {
-        using nested_iterator = typename data_store_t::iterator;
-
-        // the iterator through the underlying sparse data structure
-        nested_iterator iter;
-
-        // the currently consumed mask
-        uint64_t mask = 0;
-
-        // the value currently pointed to
-        index_type value{};
-
-    public:
-        typedef std::forward_iterator_tag iterator_category;
-        typedef index_type value_type;
-        typedef ptrdiff_t difference_type;
-        typedef value_type* pointer;
-        typedef value_type& reference;
-
-        // default constructor -- creating an end-iterator
-        iterator() = default;
-
-        iterator(const nested_iterator& iter)
-                : iter(iter), mask(toMask(iter->second)), value(iter->first << LEAF_INDEX_WIDTH) {
-            moveToNextInMask();
-        }
-
-        iterator(const nested_iterator& iter, uint64_t m, index_type value)
-                : iter(iter), mask(m), value(value) {}
-
-        // a copy constructor
-        iterator(const iterator& other) = default;
-
-        // an assignment operator
-        iterator& operator=(const iterator& other) = default;
-
-        // the equality operator as required by the iterator concept
-        bool operator==(const iterator& other) const {
-            // only equivalent if pointing to the end
-            return iter == other.iter && mask == other.mask;
-        }
-
-        // the not-equality operator as required by the iterator concept
-        bool operator!=(const iterator& other) const {
-            return !(*this == other);
-        }
-
-        // the deref operator as required by the iterator concept
-        const index_type& operator*() const {
-            return value;
-        }
-
-        // support for the pointer operator
-        const index_type* operator->() const {
-            return &value;
-        }
-
-        // the increment operator as required by the iterator concept
-        iterator& operator++() {
-            // progress in current mask
-            if (moveToNextInMask()) return *this;
-
-            // go to next entry
-            ++iter;
-
-            // update value
-            if (!iter.isEnd()) {
-                value = iter->first << LEAF_INDEX_WIDTH;
-                mask = toMask(iter->second);
-                moveToNextInMask();
-            }
-
-            // done
-            return *this;
-        }
-
-        bool isEnd() const {
-            return iter.isEnd();
-        }
-
-        void print(std::ostream& out) const {
-            out << "SparseBitMapIter(" << iter << " -> " << std::bitset<64>(mask) << " @ " << value << ")";
-        }
-
-        // enables this iterator core to be printed (for debugging)
-        friend std::ostream& operator<<(std::ostream& out, const iterator& iter) {
-            iter.print(out);
-            return out;
-        }
-
-        static uint64_t toMask(const value_t& value) {
-            static_assert(sizeof(value_t) == sizeof(uint64_t), "Fixed for 64-bit compiler.");
-            return reinterpret_cast<const uint64_t&>(value);
-        }
-
-    private:
-        bool moveToNextInMask() {
-            // check if there is something left
-            if (mask == 0) return false;
-
-            // get position of leading 1
-            auto pos = __builtin_ctzll(mask);
-
-            // consume this bit
-            mask &= ~(1llu << pos);
-
-            // update value
-            value &= ~LEAF_INDEX_MASK;
-            value |= pos;
-
-            // done
-            return true;
-        }
-    };
-
-    /**
-     * Obtains an iterator pointing to the first index set to 1. If there
-     * is no such bit, end() will be returned.
-     */
-    iterator begin() const {
-        auto it = store.begin();
-        if (it.isEnd()) return end();
-        return iterator(it);
-    }
-
-    /**
-     * Returns an iterator referencing the position after the last set bit.
-     */
-    iterator end() const {
-        return iterator();
-    }
-
-    /**
-     * Obtains an iterator referencing the position i if the corresponding
-     * bit is set, end() otherwise.
-     */
-    iterator find(index_type i) const {
-        op_context ctxt;
-        return find(i, ctxt);
-    }
-
-    /**
-     * Obtains an iterator referencing the position i if the corresponding
-     * bit is set, end() otherwise. An operation context can be provided
-     * to exploit temporal locality.
-     */
-    iterator find(index_type i, op_context& ctxt) const {
-        // check prefix part
-        auto it = store.find(i >> LEAF_INDEX_WIDTH, ctxt);
-        if (it.isEnd()) return end();
-
-        // check bit-set part
-        uint64_t mask = iterator::toMask(it->second);
-        if (!(mask & (1llu << (i & LEAF_INDEX_MASK)))) return end();
-
-        // OK, it is there => create iterator
-        mask &= ((1ull << (i & LEAF_INDEX_MASK)) - 1);  // remove all bits before pos i
-        return iterator(it, mask, i);
-    }
-
-    /**
-     * Locates an iterator to the first element in this sparse bit map not less
-     * than the given index.
-     */
-    iterator lower_bound(index_type i) const {
-        auto it = store.lowerBound(i >> LEAF_INDEX_WIDTH);
-        if (it.isEnd()) return end();
-
-        // check bit-set part
-        uint64_t mask = iterator::toMask(it->second);
-
-        // if there is no bit remaining in this mask, check next mask.
-        if (!(mask & ((~uint64_t(0)) << (i & LEAF_INDEX_MASK)))) {
-            index_type next = ((i >> LEAF_INDEX_WIDTH) + 1) << LEAF_INDEX_WIDTH;
-            if (next < i) return end();
-            return lower_bound(next);
-        }
-
-        // there are bits left, use least significant bit of those
-        if (it->first == i >> LEAF_INDEX_WIDTH) {
-            mask &= ((~uint64_t(0)) << (i & LEAF_INDEX_MASK));  // remove all bits before pos i
-        }
-
-        // compute value represented by least significant bit
-        index_type pos = __builtin_ctzll(mask);
-
-        // remove this bit as well
-        mask = mask & ~(1ull << pos);
-
-        // construct value of this located bit
-        index_type val = (it->first << LEAF_INDEX_WIDTH) | pos;
-        return iterator(it, mask, val);
-    }
-
-    /**
-     * Locates an iterator to the first element in this sparse bit map than is greater
-     * than the given index.
-     */
-    iterator upper_bound(index_type i) const {
-        if (i == std::numeric_limits<index_type>::max()) {
-            return end();
-        }
-        return lower_bound(i + 1);
-    }
-
-    /**
-     * A debugging utility printing the internal structure of this map to the
-     * given output stream.
-     */
-    void dump(bool detail = false, std::ostream& out = std::cout) const {
-        store.dump(detail, out);
-    }
-
-    /**
-     * Provides write-protected access to the internal store for running
-     * analysis on the data structure.
-     */
-    const data_store_t& getStore() const {
-        return store;
-    }
-};
-
-// ---------------------------------------------------------------------
-//                              TRIE
-// ---------------------------------------------------------------------
-
-namespace detail {
-
-/**
- * A base class for the Trie implementation allowing various
- * specializations of the Trie template to inherit common functionality.
- *
- * @tparam Dim the number of dimensions / arity of the stored tuples
- * @tparam Derived the type derived from this base class
- */
-template <unsigned Dim, typename Derived>
-class TrieBase {
-public:
-    /**
-     * The type of the stored entries / tuples.
-     */
-    using entry_type = typename souffle::Tuple<RamDomain, Dim>;
-
-    // -- operation wrappers --
-
-    /**
-     * A generic function enabling the insertion of tuple values in a user-friendly way.
-     */
-    template <typename... Values>
-    bool insert(Values... values) {
-        return static_cast<Derived&>(*this).insert(entry_type{{RamDomain(values)...}});
-    }
-
-    /**
-     * A generic function enabling the convenient conduction of a membership check.
-     */
-    template <typename... Values>
-    bool contains(Values... values) const {
-        return static_cast<const Derived&>(*this).contains(entry_type{{RamDomain(values)...}});
-    }
-
-    // ---------------------------------------------------------------------
-    //                           Iterator
-    // ---------------------------------------------------------------------
-
-    /**
-     * An iterator over the stored entries.
-     *
-     * Iterators for tries consist of a top-level iterator maintaining the
-     * master copy of a materialized tuple and a recursively nested iterator
-     * core -- one for each nested trie level.
-     */
-    template <template <unsigned D> class IterCore>
-    class iterator {
-        template <unsigned Len, unsigned Pos, unsigned Dimensions>
-        friend struct fix_binding;
-
-        template <unsigned Pos, unsigned Dimensions>
-        friend struct fix_lower_bound;
-
-        template <unsigned Pos, unsigned Dimensions>
-        friend struct fix_upper_bound;
-
-        template <unsigned Pos, unsigned Dimensions>
-        friend struct fix_first;
-
-        // the iterator core of this level
-        using iter_core_t = IterCore<0>;
-
-        // the wrapped iterator
-        iter_core_t iter_core;
-
-        // the value currently pointed to
-        entry_type value;
-
-    public:
-        typedef std::forward_iterator_tag iterator_category;
-        typedef entry_type value_type;
-        typedef ptrdiff_t difference_type;
-        typedef value_type* pointer;
-        typedef value_type& reference;
-
-        // default constructor -- creating an end-iterator
-        iterator() = default;
-
-        // a copy constructor
-        iterator(const iterator& other) = default;
-
-        iterator(iterator&& other) = default;
-
-        template <typename Param>
-        explicit iterator(const Param& param) : iter_core(param, value) {}
-
-        // an assignment operator
-        iterator& operator=(const iterator& other) = default;
-
-        // the equality operator as required by the iterator concept
-        bool operator==(const iterator& other) const {
-            // equivalent if pointing to the same value
-            return iter_core == other.iter_core;
-        }
-
-        // the not-equality operator as required by the iterator concept
-        bool operator!=(const iterator& other) const {
-            return !(*this == other);
-        }
-
-        // the deref operator as required by the iterator concept
-        const entry_type& operator*() const {
-            return value;
-        }
-
-        // support for the pointer operator
-        const entry_type* operator->() const {
-            return &value;
-        }
-
-        // the increment operator as required by the iterator concept
-        iterator& operator++() {
-            iter_core.inc(value);
-            return *this;
-        }
-
-        // enables this iterator to be printed (for debugging)
-        void print(std::ostream& out) const {
-            out << "iter(" << iter_core << " -> " << value << ")";
-        }
-
-        friend std::ostream& operator<<(std::ostream& out, const iterator& iter) {
-            iter.print(out);
-            return out;
-        }
-    };
-
-    /* -------------- operator hint statistics ----------------- */
-
-    // an aggregation of statistical values of the hint utilization
-    struct hint_statistics {
-        // the counter for insertion operations
-        CacheAccessCounter inserts;
-
-        // the counter for contains operations
-        CacheAccessCounter contains;
-
-        // the counter for get_boundaries operations
-        CacheAccessCounter get_boundaries;
-    };
-
-protected:
-    // the hint statistic of this b-tree instance
-    mutable hint_statistics hint_stats;
-
-public:
-    void printStats(std::ostream& out) const {
-        out << "---------------------------------\n";
-        out << "  insert-hint (hits/misses/total): " << hint_stats.inserts.getHits() << "/"
-            << hint_stats.inserts.getMisses() << "/" << hint_stats.inserts.getAccesses() << "\n";
-        out << "  contains-hint (hits/misses/total):" << hint_stats.contains.getHits() << "/"
-            << hint_stats.contains.getMisses() << "/" << hint_stats.contains.getAccesses() << "\n";
-        out << "  get-boundaries-hint (hits/misses/total):" << hint_stats.get_boundaries.getHits() << "/"
-            << hint_stats.get_boundaries.getMisses() << "/" << hint_stats.get_boundaries.getAccesses()
-            << "\n";
-        out << "---------------------------------\n";
-    }
-};
-
-/**
- * A functor extracting a reference to a nested iterator core from an enclosing
- * iterator core.
- */
-template <unsigned Level>
-struct get_nested_iter_core {
-    template <typename IterCore>
-    auto operator()(IterCore& core) -> decltype(get_nested_iter_core<Level - 1>()(core.getNested())) {
-        return get_nested_iter_core<Level - 1>()(core.getNested());
-    }
-};
-
-template <>
-struct get_nested_iter_core<0> {
-    template <typename IterCore>
-    IterCore& operator()(IterCore& core) {
-        return core;
-    }
-};
-
-/**
- * A functor initializing an iterator upon creation to reference the first
- * element in the associated Trie.
- */
-template <unsigned Pos, unsigned Dim>
-struct fix_first {
-    template <unsigned bits, typename iterator>
-    void operator()(const SparseBitMap<bits>& store, iterator& iter) const {
-        // set iterator to first in store
-        auto first = store.begin();
-        get_nested_iter_core<Pos>()(iter.iter_core).setIterator(first);
-        iter.value[Pos] = *first;
-    }
-
-    template <typename Store, typename iterator>
-    void operator()(const Store& store, iterator& iter) const {
-        // set iterator to first in store
-        auto first = store.begin();
-        get_nested_iter_core<Pos>()(iter.iter_core).setIterator(first);
-        iter.value[Pos] = first->first;
-        // and continue recursively
-        fix_first<Pos + 1, Dim>()(first->second->getStore(), iter);
-    }
-};
-
-template <unsigned Dim>
-struct fix_first<Dim, Dim> {
-    template <typename Store, typename iterator>
-    void operator()(const Store&, iterator&) const {
-        // terminal case => nothing to do
-    }
-};
-
-/**
- * A functor initializing an iterator upon creation to reference the first element
- * exhibiting a given prefix within a given Trie.
- */
-template <unsigned Len, unsigned Pos, unsigned Dim>
-struct fix_binding {
-    template <unsigned bits, typename iterator, typename entry_type>
-    bool operator()(
-            const SparseBitMap<bits>& store, iterator& begin, iterator& end, const entry_type& entry) const {
-        // search in current level
-        auto cur = store.find(entry[Pos]);
-
-        // if not present => fail
-        if (cur == store.end()) return false;
-
-        // take current value
-        get_nested_iter_core<Pos>()(begin.iter_core).setIterator(cur);
-        ++cur;
-        get_nested_iter_core<Pos>()(end.iter_core).setIterator(cur);
-
-        // update iterator value
-        begin.value[Pos] = entry[Pos];
-
-        // no more remaining levels to fix
-        return true;
-    }
-
-    template <typename Store, typename iterator, typename entry_type>
-    bool operator()(const Store& store, iterator& begin, iterator& end, const entry_type& entry) const {
-        // search in current level
-        auto cur = store.find(entry[Pos]);
-
-        // if not present => fail
-        if (cur == store.end()) return false;
-
-        // take current value as start
-        get_nested_iter_core<Pos>()(begin.iter_core).setIterator(cur);
-
-        // update iterator value
-        begin.value[Pos] = entry[Pos];
-
-        // fix remaining nested iterators
-        auto res = fix_binding<Len - 1, Pos + 1, Dim>()(cur->second->getStore(), begin, end, entry);
-
-        // update end of iterator
-        if (get_nested_iter_core<Pos + 1>()(end.iter_core).getIterator() == cur->second->getStore().end()) {
-            ++cur;
-            if (cur != store.end()) {
-                fix_first<Pos + 1, Dim>()(cur->second->getStore(), end);
-            }
-        }
-        get_nested_iter_core<Pos>()(end.iter_core).setIterator(cur);
-
-        // done
-        return res;
-    }
-};
-
-template <unsigned Pos, unsigned Dim>
-struct fix_binding<0, Pos, Dim> {
-    template <unsigned bits, typename iterator, typename entry_type>
-    bool operator()(const SparseBitMap<bits>& store, iterator& begin, iterator& /* end */,
-            const entry_type& /* entry */) const {
-        // move begin to begin of store
-        auto a = store.begin();
-        get_nested_iter_core<Pos>()(begin.iter_core).setIterator(a);
-        begin.value[Pos] = *a;
-
-        return true;
-    }
-
-    template <typename Store, typename iterator, typename entry_type>
-    bool operator()(const Store& store, iterator& begin, iterator& end, const entry_type& entry) const {
-        // move begin to begin of store
-        auto a = store.begin();
-        get_nested_iter_core<Pos>()(begin.iter_core).setIterator(a);
-        begin.value[Pos] = a->first;
-
-        // continue recursively
-        fix_binding<0, Pos + 1, Dim>()(a->second->getStore(), begin, end, entry);
-        return true;
-    }
-};
-
-template <unsigned Dim>
-struct fix_binding<0, Dim, Dim> {
-    template <typename Store, typename iterator, typename entry_type>
-    bool operator()(const Store& /* store */, iterator& /* begin */, iterator& /* end */,
-            const entry_type& /* entry */) const {
-        // nothing more to do
-        return true;
-    }
-};
-
-/**
- * A functor initializing an iterator upon creation to reference the first element
- * within a given Trie being not less than a given value .
- */
-template <unsigned Pos, unsigned Dim>
-struct fix_lower_bound {
-    template <unsigned bits, typename iterator, typename entry_type>
-    bool operator()(const SparseBitMap<bits>& store, iterator& iter, const entry_type& entry) const {
-        // search in current level
-        auto cur = store.lower_bound(entry[Pos]);
-
-        if (cur == store.end()) return false;
-
-        get_nested_iter_core<Pos>()(iter.iter_core).setIterator(cur);
-
-        assert(entry[Pos] <= RamDomain(*cur));
-        iter.value[Pos] = *cur;
-
-        // no more remaining levels to fix
-        return true;
-    }
-
-    template <typename Store, typename iterator, typename entry_type>
-    bool operator()(const Store& store, iterator& iter, const entry_type& entry) const {
-        // search in current level
-        auto cur = store.lowerBound(entry[Pos]);
-
-        // if no lower boundary is found, be done
-        if (cur == store.end()) return false;
-        assert(RamDomain(cur->first) >= entry[Pos]);
-
-        // if the lower bound is higher than the requested value, go to first in subtree
-        if (RamDomain(cur->first) > entry[Pos]) {
-            get_nested_iter_core<Pos>()(iter.iter_core).setIterator(cur);
-            iter.value[Pos] = cur->first;
-            fix_first<Pos + 1, Dim>()(cur->second->getStore(), iter);
-            return true;
-        }
-
-        // attempt to fix the rest
-        if (!fix_lower_bound<Pos + 1, Dim>()(cur->second->getStore(), iter, entry)) {
-            // if it does not work, since there are no matching elements in this branch, go to next
-            entry_type sub = entry;
-            sub[Pos] += 1;
-            for (size_t i = Pos + 1; i < Dim; ++i) {
-                sub[i] = 0;
-            }
-            return (*this)(store, iter, sub);
-        }
-
-        // remember result
-        get_nested_iter_core<Pos>()(iter.iter_core).setIterator(cur);
-
-        // update iterator value
-        iter.value[Pos] = cur->first;
-
-        // done!
-        return true;
-    }
-};
-
-/**
- * A functor initializing an iterator upon creation to reference the first element
- * within a given Trie being greater than a given value .
- */
-template <unsigned Pos, unsigned Dim>
-struct fix_upper_bound {
-    template <unsigned bits, typename iterator, typename entry_type>
-    bool operator()(const SparseBitMap<bits>& store, iterator& iter, const entry_type& entry) const {
-        // search in current level
-        auto cur = store.upper_bound(entry[Pos]);
-
-        if (cur == store.end()) {
-            return false;
-        }
-
-        get_nested_iter_core<Pos>()(iter.iter_core).setIterator(cur);
-
-        assert(entry[Pos] <= RamDomain(*cur));
-        iter.value[Pos] = *cur;
-
-        // no more remaining levels to fix
-        return true;
-    }
-
-    template <typename Store, typename iterator, typename entry_type>
-    bool operator()(const Store& store, iterator& iter, const entry_type& entry) const {
-        // search in current level (if it is not the last level, we need a lower bound)
-        auto cur = store.lowerBound(entry[Pos]);
-
-        // if no lower boundary is found, be done
-        if (cur == store.end()) {
-            return false;
-        }
-        assert(RamDomain(cur->first) >= entry[Pos]);
-
-        // if the lower bound is higher than the requested value, go to first in subtree
-        if (RamDomain(cur->first) > entry[Pos]) {
-            get_nested_iter_core<Pos>()(iter.iter_core).setIterator(cur);
-            iter.value[Pos] = cur->first;
-            fix_first<Pos + 1, Dim>()(cur->second->getStore(), iter);
-            return true;
-        }
-
-        // attempt to fix the rest
-        if (!fix_upper_bound<Pos + 1, Dim>()(cur->second->getStore(), iter, entry)) {
-            // if it does not work, since there are no matching elements in this branch, go to next
-            entry_type sub = entry;
-            sub[Pos] += 1;
-            for (size_t i = Pos + 1; i < Dim; ++i) {
-                sub[i] = 0;
-            }
-            return (*this)(store, iter, sub);
-        }
-
-        // remember result
-        get_nested_iter_core<Pos>()(iter.iter_core).setIterator(cur);
-
-        // update iterator value
-        iter.value[Pos] = cur->first;
-
-        // done!
-        return true;
-    }
-};
-
-}  // namespace detail
-
-/**
- * The most generic implementation of a Trie forming the top-level of any
- * Trie storing tuples of arity > 1.
- */
-template <unsigned Dim>
-class Trie : public souffle::detail::TrieBase<Dim, Trie<Dim>> {
-    template <unsigned D>
-    friend class Trie;
-
-    template <unsigned D, typename Derived>
-    friend class TrieBase;
-
-    // a shortcut for the common base class type
-    using base = typename souffle::detail::TrieBase<Dim, Trie<Dim>>;
-
-    // the type of the nested tries (1 dimension less)
-    using nested_trie_type = Trie<Dim - 1>;
-
-    // the merge operation capable of merging two nested tries
-    struct nested_trie_merger {
-        nested_trie_type* operator()(nested_trie_type* a, const nested_trie_type* b) const {
-            if (!b) return a;
-            if (!a) return new nested_trie_type(*b);
-            a->insertAll(*b);
-            return a;
-        }
-    };
-
-    // the operation capable of cloning a nested trie
-    struct nested_trie_cloner {
-        nested_trie_type* operator()(nested_trie_type* a) const {
-            if (!a) return a;
-            return new nested_trie_type(*a);
-        }
-    };
-
-    // the data structure utilized for indexing nested tries
-    using store_type = SparseArray<nested_trie_type*,
-            6,  // = 2^6 entries per block
-            nested_trie_merger, nested_trie_cloner>;
-
-    // the actual data store
-    store_type store;
-
-public:
-    using entry_type = typename souffle::Tuple<RamDomain, Dim>;
-    using element_type = entry_type;
-
-    // ---------------------------------------------------------------------
-    //                           Iterator
-    // ---------------------------------------------------------------------
-
-    /**
-     * The iterator core for trie iterators involving this level.
-     */
-    template <unsigned I = 0>
-    class iterator_core {
-        // the iterator for the current level
-        using store_iter_t = typename store_type::iterator;
-
-        // the type of the nested iterator
-        using nested_iter_core = typename Trie<Dim - 1>::template iterator_core<I + 1>;
-
-        store_iter_t iter;
-
-        nested_iter_core nested;
-
-    public:
-        /** default end-iterator constructor */
-        iterator_core() = default;
-
-        template <typename Tuple>
-        iterator_core(const store_iter_t& iter, Tuple& entry) : iter(iter) {
-            entry[I] = iter->first;
-            nested = iter->second->template getBeginCoreIterator<I + 1>(entry);
-        }
-
-        void setIterator(const store_iter_t& iter) {
-            this->iter = iter;
-        }
-
-        store_iter_t& getIterator() {
-            return this->iter;
-        }
-
-        nested_iter_core& getNested() {
-            return nested;
-        }
-
-        template <typename Tuple>
-        bool inc(Tuple& entry) {
-            // increment nested iterator
-            if (nested.inc(entry)) return true;
-
-            // increment the iterator on this level
-            ++iter;
-
-            // check whether the end has been reached
-            if (iter.isEnd()) return false;
-
-            // otherwise update entry value
-            entry[I] = iter->first;
-
-            // and restart nested
-            nested = iter->second->template getBeginCoreIterator<I + 1>(entry);
-            return true;
-        }
-
-        bool operator==(const iterator_core& other) const {
-            return nested == other.nested && iter == other.iter;
-        }
-
-        bool operator!=(const iterator_core& other) const {
-            return !(*this == other);
-        }
-
-        // enables this iterator core to be printed (for debugging)
-        void print(std::ostream& out) const {
-            out << iter << " | " << nested;
-        }
-
-        friend std::ostream& operator<<(std::ostream& out, const iterator_core& iter) {
-            iter.print(out);
-            return out;
-        }
-    };
-
-    // the type of iterator to be utilized when iterating of instances of this trie
-    using iterator = typename base::template iterator<iterator_core>;
-
-    // the operation context aggregating all operation contexts of nested structures
-    struct op_context {
-        using local_ctxt = typename store_type::op_context;
-        using nested_ctxt = typename nested_trie_type::op_context;
-
-        // for insert and contain
-        local_ctxt local{};
-        RamDomain lastQuery{};
-        nested_trie_type* lastNested{nullptr};
-        nested_ctxt nestedCtxt{};
-
-        // for boundaries
-        unsigned lastBoundaryLevels{Dim + 1};
-        entry_type lastBoundaryRequest{};
-        range<iterator> lastBoundaries{iterator(), iterator()};
-
-        op_context() = default;
-    };
-
-    using operation_hints = op_context;
-
-    using base::contains;
-    using base::insert;
-
-    /**
-     * A simple destructore.
-     */
-    ~Trie() {
-        for (auto& cur : store) {
-            delete cur.second;  // clears all nested tries
-        }
-    }
-
-    /**
-     * Determines whether this trie is empty or not.
-     */
-    bool empty() const {
-        return store.empty();
-    }
-
-    /**
-     * Determines the number of entries in this trie.
-     */
-    std::size_t size() const {
-        // the number of elements is lazy-evaluated
-        std::size_t res = 0;
-        for (const auto& cur : store) {
-            res += cur.second->size();
-        }
-        return res;
-    }
-
-    /**
-     * Computes the total memory usage of this data structure.
-     */
-    std::size_t getMemoryUsage() const {
-        // compute the total memory usage of this level
-        std::size_t res = sizeof(*this) - sizeof(store) + store.getMemoryUsage();
-
-        // add the memory usage of sub-levels
-        for (const auto& cur : store) {
-            res += cur.second->getMemoryUsage();
-        }
-
-        // done
-        return res;
-    }
-
-    /**
-     * Removes all entries within this trie.
-     */
-    void clear() {
-        // delete lower levels
-        for (auto& cur : store) {
-            delete cur.second;
-        }
-
-        // clear store
-        store.clear();
-    }
-
-    /**
-     * Inserts a new entry.
-     *
-     * @param tuple the entry to be added
-     * @return true if the same tuple hasn't been present before, false otherwise
-     */
-    bool insert(const entry_type& tuple) {
-        op_context ctxt;
-        return insert(tuple, ctxt);
-    }
-
-    /**
-     * Inserts a new entry. A operation context may be provided to exploit temporal
-     * locality.
-     *
-     * @param tuple the entry to be added
-     * @param ctxt the operation context to be utilized
-     * @return true if the same tuple hasn't been present before, false otherwise
-     */
-    bool insert(const entry_type& tuple, op_context& ctxt) {
-        return insert_internal<0>(tuple, ctxt);
-    }
-
-    /**
-     * Determines whether a given tuple is present within the set specified
-     * by this trie.
-     *
-     * @param tuple the tuple to be tested
-     * @return true if present, false otherwise
-     */
-    bool contains(const entry_type& tuple) const {
-        op_context ctxt;
-        return contains(tuple, ctxt);
-    }
-
-    /**
-     * Determines whether a given tuple is present within the set specified
-     * by this trie. A operation context may be provided to exploit temporal
-     * locality.
-     *
-     * @param tuple the entry to be added
-     * @param ctxt the operation context to be utilized
-     * @return true if the same tuple hasn't been present before, false otherwise
-     */
-    bool contains(const entry_type& tuple, op_context& ctxt) const {
-        return contains_internal<0>(tuple, ctxt);
-    }
-
-    /**
-     * Inserts all elements stored within the given trie into this trie.
-     *
-     * @param other the elements to be inserted into this trie
-     */
-    void insertAll(const Trie& other) {
-        store.addAll(other.store);
-    }
-
-    /**
-     * Obtains an iterator referencing the first element stored within this trie.
-     */
-    iterator begin() const {
-        auto it = store.begin();
-        if (it.isEnd()) return end();
-        return iterator(it);
-    }
-
-    /**
-     * Obtains an iterator referencing the position after the last element stored
-     * within this trie.
-     */
-    iterator end() const {
-        return iterator();
-    }
-
-    iterator find(const entry_type& entry) const {
-        op_context ctxt;
-        return find(entry, ctxt);
-    }
-
-    iterator find(const entry_type& entry, op_context& ctxt) const {
-        auto range = getBoundaries<Dim>(entry, ctxt);
-        return (!range.empty()) ? range.begin() : end();
-    }
-
-    /**
-     * Obtains a range of elements matching the prefix of the given entry up to
-     * levels elements.
-     *
-     * @tparam levels the length of the requested matching prefix
-     * @param entry the entry to be looking for
-     * @return the corresponding range of matching elements
-     */
-    template <unsigned levels>
-    range<iterator> getBoundaries(const entry_type& entry) const {
-        op_context ctxt;
-        return getBoundaries<levels>(entry, ctxt);
-    }
-
-    /**
-     * Obtains a range of elements matching the prefix of the given entry up to
-     * levels elements. A operation context may be provided to exploit temporal
-     * locality.
-     *
-     * @tparam levels the length of the requested matching prefix
-     * @param entry the entry to be looking for
-     * @param ctxt the operation context to be utilized
-     * @return the corresponding range of matching elements
-     */
-    template <unsigned levels>
-    range<iterator> getBoundaries(const entry_type& entry, op_context& ctxt) const {
-        // if nothing is bound => just use begin and end
-        if (levels == 0) return make_range(begin(), end());
-
-        // check context
-        if (ctxt.lastBoundaryLevels == levels) {
-            bool fit = true;
-            for (unsigned i = 0; i < levels; ++i) {
-                fit = fit && (entry[i] == ctxt.lastBoundaryRequest[i]);
-            }
-
-            // if it fits => take it
-            if (fit) {
-                base::hint_stats.get_boundaries.addHit();
-                return ctxt.lastBoundaries;
-            }
-        }
-
-        // the hint has not been a hit
-        base::hint_stats.get_boundaries.addMiss();
-
-        // start with two end iterators
-        iterator begin{};
-        iterator end{};
-
-        // adapt them level by level
-        auto found = souffle::detail::fix_binding<levels, 0, Dim>()(store, begin, end, entry);
-        if (!found) return make_range(iterator(), iterator());
-
-        // update context
-        ctxt.lastBoundaryLevels = levels;
-        ctxt.lastBoundaryRequest = entry;
-        ctxt.lastBoundaries = make_range(begin, end);
-
-        // use the result
-        return ctxt.lastBoundaries;
-    }
-
-    /**
-     * Obtains an iterator to the first element not less than the given entry value.
-     *
-     * @param entry the lower bound for this search
-     * @param ctxt the operation context to be utilized
-     * @return an iterator addressing the first element in this structure not less than the given value
-     */
-    iterator lower_bound(const entry_type& entry, op_context& /* ctxt */) const {
-        // start with a default-initialized iterator
-        iterator res;
-
-        // adapt it level by level
-        bool found = detail::fix_lower_bound<0, Dim>()(store, res, entry);
-
-        // use the result
-        return found ? res : end();
-    }
-
-    /**
-     * Obtains an iterator to the first element not less than the given entry value.
-     *
-     * @param entry the lower bound for this search
-     * @return an iterator addressing the first element in this structure not less than the given value
-     */
-    iterator lower_bound(const entry_type& entry) const {
-        op_context ctxt;
-        return lower_bound(entry, ctxt);
-    }
-
-    /**
-     * Obtains an iterator to the first element greater than the given entry value, or end if there is no such
-     * element.
-     *
-     * @param entry the upper bound for this search
-     * @param ctxt the operation context to be utilized
-     * @return an iterator addressing the first element in this structure greater than the given value
-     */
-    iterator upper_bound(const entry_type& entry, op_context& /* ctxt */) const {
-        // start with a default-initialized iterator
-        iterator res;
-
-        // adapt it level by level
-        bool found = detail::fix_upper_bound<0, Dim>()(store, res, entry);
-
-        // use the result
-        return found ? res : end();
-    }
-
-    /**
-     * Obtains an iterator to the first element greater than the given entry value, or end if there is no such
-     * element.
-     *
-     * @param entry the upper bound for this search
-     * @return an iterator addressing the first element in this structure greater than the given value
-     */
-    iterator upper_bound(const entry_type& entry) const {
-        op_context ctxt;
-        return upper_bound(entry, ctxt);
-    }
-
-    /**
-     * Computes a partition of an approximate number of chunks of the content
-     * of this trie. Thus, the union of the resulting set of disjoint ranges is
-     * equivalent to the content of this trie.
-     *
-     * @param chunks the number of chunks requested
-     * @return a list of sub-ranges forming a partition of the content of this trie
-     */
-    std::vector<range<iterator>> partition(unsigned chunks = 500) const {
-        std::vector<range<iterator>> res;
-
-        // shortcut for empty trie
-        if (this->empty()) return res;
-
-        // use top-level elements for partitioning
-        int step = std::max(store.size() / chunks, size_t(1));
-
-        int c = 1;
-        auto priv = begin();
-        for (auto it = store.begin(); it != store.end(); ++it, c++) {
-            if (c % step != 0 || c == 1) {
-                continue;
-            }
-            auto cur = iterator(it);
-            res.push_back(make_range(priv, cur));
-            priv = cur;
-        }
-        // add final chunk
-        res.push_back(make_range(priv, end()));
-        return res;
-    }
-
-    /**
-     * Provides a protected access to the internally maintained store.
-     */
-    const store_type& getStore() const {
-        return store;
-    }
-
-private:
-    /**
-     * Creates a core iterator for this trie level and updates component
-     * I of the given entry to exhibit the corresponding first value.
-     *
-     * @tparam I the index of the tuple to be processed by the resulting iterator core
-     * @tparam Tuple the type of the tuple to be processed by the resulting iterator core
-     * @param entry a reference to the tuple to be updated to the first value
-     * @return the requested iterator core instance
-     */
-    template <unsigned I, typename Tuple>
-    iterator_core<I> getBeginCoreIterator(Tuple& entry) const {
-        return iterator_core<I>(store.begin(), entry);
-    }
-
-    /**
-     * The internally utilized implementation of the insert operation inserting
-     * a given tuple into this sub-trie.
-     *
-     * @tparam I the component index associated to this level
-     * @tparam Tuple the tuple type to be inserted
-     * @param tuple the tuple to be inserted
-     * @param ctxt a operation context to exploit temporal locality
-     * @return true if this tuple wasn't contained before, false otherwise
-     */
-    template <unsigned I, typename Tuple>
-    bool insert_internal(const Tuple& tuple, op_context& ctxt) {
-        using value_t = typename store_type::value_type;
-        using atomic_value_t = typename store_type::atomic_value_type;
-
-        // check context
-        if (ctxt.lastNested && ctxt.lastQuery == tuple[I]) {
-            base::hint_stats.inserts.addHit();
-            return ctxt.lastNested->template insert_internal<I + 1>(tuple, ctxt.nestedCtxt);
-        } else {
-            base::hint_stats.inserts.addMiss();
-        }
-
-        // lookup nested
-        atomic_value_t& next = store.getAtomic(tuple[I], ctxt.local);
-
-        // get pure pointer to next level
-        value_t nextPtr = next;
-
-        // conduct a lock-free lazy-creation of nested trees
-        if (!nextPtr) {
-            // create a new sub-tree
-            auto newNested = new nested_trie_type();
-
-            // register new sub-tree atomically
-            if (next.compare_exchange_weak(nextPtr, newNested)) {
-                nextPtr = newNested;  // worked
-            } else {
-                delete newNested;  // some other thread was faster => use its version
-            }
-        }
-
-        // make sure a next has been established
-        assert(nextPtr);
-
-        // clear context if necessary
-        if (nextPtr != ctxt.lastNested) {
-            ctxt.lastQuery = tuple[I];
-            ctxt.lastNested = nextPtr;
-            ctxt.nestedCtxt = typename op_context::nested_ctxt();
-        }
-
-        // conduct recursive step
-        return nextPtr->template insert_internal<I + 1>(tuple, ctxt.nestedCtxt);
-    }
-
-    /**
-     * An internal implementation of the contains member function determining
-     * whether a given tuple is present within this sub-trie or not.
-     *
-     * @tparam I the component index associated to this level
-     * @tparam Tuple the tuple type to be checked
-     * @param tuple the tuple to be checked
-     * @param ctxt a operation context to exploit temporal locality
-     * @return true if this tuple is present, false otherwise
-     */
-    template <unsigned I, typename Tuple>
-    bool contains_internal(const Tuple& tuple, op_context& ctxt) const {
-        // check context
-        if (ctxt.lastNested && ctxt.lastQuery == tuple[I]) {
-            base::hint_stats.contains.addHit();
-            return ctxt.lastNested->template contains_internal<I + 1>(tuple, ctxt.nestedCtxt);
-        } else {
-            base::hint_stats.contains.addMiss();
-        }
-
-        // lookup next step
-        auto next = store.lookup(tuple[I], ctxt.local);
-
-        // clear context if necessary
-        if (next != ctxt.lastNested) {
-            ctxt.lastQuery = tuple[I];
-            ctxt.lastNested = next;
-            ctxt.nestedCtxt = typename op_context::nested_ctxt();
-        }
-
-        // conduct recursive step
-        return next && next->template contains_internal<I + 1>(tuple, ctxt.nestedCtxt);
-    }
-};
-
-/**
- * A template specialization for tries representing a set.
- * For improved memory efficiency, this level is the leaf-node level
- * of all tries exhibiting an arity >= 1. Internally, values are stored utilizing
- * sparse bit maps.
- */
-template <>
-class Trie<1u> : public detail::TrieBase<1u, Trie<1u>> {
-    template <unsigned Dim>
-    friend class Trie;
-
-    template <unsigned Dim, typename Derived>
-    friend class detail::TrieBase;
-
-    // a shortcut for the base type
-    using base = typename detail::TrieBase<1u, Trie<1u>>;
-
-    // the map type utilized internally
-    using map_type = SparseBitMap<>;
-
-    // the internal data store
-    map_type map;
-
-public:
-    using element_type = entry_type;
-    using op_context = typename map_type::op_context;
-    using operation_hints = op_context;
-
-    using base::contains;
-    using base::insert;
-
-    /**
-     * Determines whether this trie is empty or not.
-     */
-    bool empty() const {
-        return map.empty();
-    }
-
-    /**
-     * Determines the number of elements stored in this trie.
-     */
-    std::size_t size() const {
-        return map.size();
-    }
-
-    /**
-     * Computes the total memory usage of this data structure.
-     */
-    std::size_t getMemoryUsage() const {
-        // compute the total memory usage
-        return sizeof(*this) - sizeof(map_type) + map.getMemoryUsage();
-    }
-
-    /**
-     * Removes all elements form this trie.
-     */
-    void clear() {
-        map.clear();
-    }
-
-    /**
-     * Inserts the given tuple into this trie.
-     *
-     * @param tuple the tuple to be inserted
-     * @return true if the tuple has not been present before, false otherwise
-     */
-    bool insert(const entry_type& tuple) {
-        op_context ctxt;
-        return insert(tuple, ctxt);
-    }
-
-    /**
-     * Inserts the given tuple into this trie.
-     * An operation context can be provided to exploit temporal locality.
-     *
-     * @param tuple the tuple to be inserted
-     * @param ctxt an operation context for exploiting temporal locality
-     * @return true if the tuple has not been present before, false otherwise
-     */
-    bool insert(const entry_type& tuple, op_context& ctxt) {
-        return insert_internal<0>(tuple, ctxt);
-    }
-
-    /**
-     * Determines whether the given tuple is present in this trie or not.
-     *
-     * @param tuple the tuple to be tested
-     * @return true if present, false otherwise
-     */
-    bool contains(const entry_type& tuple) const {
-        op_context ctxt;
-        return contains(tuple, ctxt);
-    }
-
-    /**
-     * Determines whether the given tuple is present in this trie or not.
-     * An operation context can be provided to exploit temporal locality.
-     *
-     * @param tuple the tuple to be tested
-     * @param ctxt an operation context for exploiting temporal locality
-     * @return true if present, false otherwise
-     */
-    bool contains(const entry_type& tuple, op_context& ctxt) const {
-        return contains_internal<0>(tuple, ctxt);
-    }
-
-    /**
-     * Inserts all tuples stored within the given trie into this trie.
-     * This operation is considerably more efficient than the consecutive
-     * insertion of the elements in other into this trie.
-     */
-    void insertAll(const Trie& other) {
-        map.addAll(other.map);
-    }
-
-    // ---------------------------------------------------------------------
-    //                           Iterator
-    // ---------------------------------------------------------------------
-
-    /**
-     * The iterator core of this level contributing to the construction of
-     * a composed trie iterator.
-     */
-    template <unsigned I = 0>
-    class iterator_core {
-        // the iterator for this level
-        using iter_type = typename map_type::iterator;
-
-        // the referenced bit-map iterator
-        iter_type iter;
-
-    public:
-        /** default end-iterator constructor */
-        iterator_core() = default;
-
-        template <typename Tuple>
-        iterator_core(const iter_type& iter, Tuple& entry) : iter(iter) {
-            entry[I] = static_cast<RamDomain>(*iter);
-        }
-
-        void setIterator(const iter_type& iter) {
-            this->iter = iter;
-        }
-
-        iter_type& getIterator() {
-            return this->iter;
-        }
-
-        template <typename Tuple>
-        bool inc(Tuple& entry) {
-            // increment the iterator on this level
-            ++iter;
-
-            // check whether the end has been reached
-            if (iter.isEnd()) return false;
-
-            // otherwise update entry value
-            entry[I] = *iter;
-            return true;
-        }
-
-        bool operator==(const iterator_core& other) const {
-            return iter == other.iter;
-        }
-
-        bool operator!=(const iterator_core& other) const {
-            return !(*this == other);
-        }
-
-        // enables this iterator core to be printed (for debugging)
-        void print(std::ostream& out) const {
-            out << iter;
-        }
-
-        friend std::ostream& operator<<(std::ostream& out, const iterator_core& iter) {
-            iter.print(out);
-            return out;
-        }
-    };
-
-    // the iterator type utilized by this trie type
-    using iterator = typename base::template iterator<iterator_core>;
-
-    /**
-     * Obtains an iterator referencing the first element stored within this trie
-     * or end() if this trie is empty.
-     */
-    iterator begin() const {
-        if (map.empty()) return end();
-        return iterator(map.begin());
-    }
-
-    /**
-     * Obtains an iterator referencing the first position after the last element
-     * within this trie.
-     */
-    iterator end() const {
-        return iterator();
-    }
-
-    /**
-     * Obtains a partition of this tire such that the resulting list of ranges
-     * cover disjoint subsets of the elements stored in this trie. Their union
-     * is equivalent to the content of this trie.
-     */
-    std::vector<range<iterator>> partition(unsigned chunks = 500) const {
-        std::vector<range<iterator>> res;
-
-        // shortcut for empty trie
-        if (this->empty()) return res;
-
-        // use top-level elements for partitioning
-        int step = static_cast<int>(std::max(map.size() / chunks, size_t(1)));
-
-        int c = 1;
-        auto priv = begin();
-        for (auto it = map.begin(); it != map.end(); ++it, c++) {
-            if (c % step != 0 || c == 1) {
-                continue;
-            }
-            auto cur = iterator(it);
-            res.push_back(make_range(priv, cur));
-            priv = cur;
-        }
-        // add final chunk
-        res.push_back(make_range(priv, end()));
-        return res;
-    }
-
-    /**
-     * Obtains a range of elements matching the prefix of the given entry up to
-     * levels elements.
-     *
-     * @tparam levels the length of the requested matching prefix
-     * @param entry the entry to be looking for
-     * @return the corresponding range of matching elements
-     */
-    template <unsigned levels>
-    range<iterator> getBoundaries(const entry_type& entry) const {
-        op_context ctxt;
-        return getBoundaries<levels>(entry, ctxt);
-    }
-
-    /**
-     * Obtains a range of elements matching the prefix of the given entry up to
-     * levels elements. A operation context may be provided to exploit temporal
-     * locality.
-     *
-     * @tparam levels the length of the requested matching prefix
-     * @param entry the entry to be looking for
-     * @param ctxt the operation context to be utilized
-     * @return the corresponding range of matching elements
-     */
-    template <unsigned levels>
-    range<iterator> getBoundaries(const entry_type& entry, op_context& ctxt) const {
-        // for levels = 0
-        if (levels == 0) return make_range(begin(), end());
-        // for levels = 1
-        auto pos = map.find(entry[0], ctxt);
-        if (pos == map.end()) return make_range(end(), end());
-        auto next = pos;
-        ++next;
-        return make_range(iterator(pos), iterator(next));
-    }
-
-    iterator lower_bound(const entry_type& entry, op_context&) const {
-        return iterator(map.lower_bound(entry[0]));
-    }
-
-    iterator lower_bound(const entry_type& entry) const {
-        op_context ctxt;
-        return lower_bound(entry, ctxt);
-    }
-
-    iterator upper_bound(const entry_type& entry, op_context&) const {
-        return iterator(map.upper_bound(entry[0]));
-    }
-
-    iterator upper_bound(const entry_type& entry) const {
-        op_context ctxt;
-        return upper_bound(entry, ctxt);
-    }
-
-    /**
-     * Provides protected access to the internally maintained store.
-     */
-    const map_type& getStore() const {
-        return map;
-    }
-
-private:
-    /**
-     * Creates a core iterator for this trie level and updates component
-     * I of the given entry to exhibit the corresponding first value.
-     *
-     * @tparam I the index of the tuple to be processed by the resulting iterator core
-     * @tparam Tuple the type of the tuple to be processed by the resulting iterator core
-     * @param entry a reference to the tuple to be updated to the first value
-     * @return the requested iterator core instance
-     */
-    template <unsigned I, typename Tuple>
-    iterator_core<I> getBeginCoreIterator(Tuple& entry) const {
-        return iterator_core<I>(map.begin(), entry);
-    }
-
-    /**
-     * The internally utilized implementation of the insert operation inserting
-     * a given tuple into this sub-trie.
-     *
-     * @tparam I the component index associated to this level
-     * @tparam Tuple the tuple type to be inserted
-     * @param tuple the tuple to be inserted
-     * @param ctxt a operation context to exploit temporal locality
-     * @return true if this tuple wasn't contained before, false otherwise
-     */
-    template <unsigned I, typename Tuple>
-    bool insert_internal(const Tuple& tuple, op_context& ctxt) {
-        return map.set(tuple[I], ctxt);
-    }
-
-    /**
-     * An internal implementation of the contains member function determining
-     * whether a given tuple is present within this sub-trie or not.
-     *
-     * @tparam I the component index associated to this level
-     * @tparam Tuple the tuple type to be checked
-     * @param tuple the tuple to be checked
-     * @param ctxt a operation context to exploit temporal locality
-     * @return true if this tuple is present, false otherwise
-     */
-    template <unsigned I, typename Tuple>
-    bool contains_internal(const Tuple& tuple, op_context& ctxt) const {
-        return map.test(tuple[I], ctxt);
-    }
-};
-
-}  // end namespace souffle
+#include "souffle/RamTypes.h"
+#include "souffle/utility/CacheUtil.h"
+#include "souffle/utility/ContainerUtil.h"
+#include "souffle/utility/MiscUtil.h"
+#include "souffle/utility/StreamUtil.h"
+#include "souffle/utility/span.h"
+#include <algorithm>
+#include <atomic>
+#include <bitset>
+#include <cassert>
+#include <climits>
+#include <cstdint>
+#include <cstring>
+#include <iostream>
+#include <iterator>
+#include <limits>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+// TODO: replace intrinsics w/ std lib functions?
+#ifdef _WIN32
+/**
+ * When compiling for windows, redefine the gcc builtins which are used to
+ * their equivalents on the windows platform.
+ */
+#define __sync_synchronize MemoryBarrier
+#define __sync_bool_compare_and_swap(ptr, oldval, newval) \
+    (InterlockedCompareExchangePointer((void* volatile*)ptr, (void*)newval, (void*)oldval) == (void*)oldval)
+#endif  // _WIN32
+
+namespace souffle {
+
+template <unsigned Dim>
+class Trie;
+
+namespace detail::brie {
+
+// FIXME: These data structs should be parameterised/made agnostic to `RamDomain` type.
+using brie_element_type = RamDomain;
+
+using tcb::make_span;
+
+template <typename A>
+struct forward_non_output_iterator_traits {
+    using value_type = A;
+    using difference_type = ptrdiff_t;
+    using iterator_category = std::forward_iterator_tag;
+    using pointer = const value_type*;
+    using reference = const value_type&;
+};
+
+template <typename A, std::size_t arity>
+auto copy(span<A, arity> s) {
+    std::array<std::decay_t<A>, arity> cpy;
+    std::copy_n(s.begin(), arity, cpy.begin());
+    return cpy;
+}
+
+template <std::size_t offset, typename A, std::size_t arity>
+auto drop(span<A, arity> s) -> std::enable_if_t<offset <= arity, span<A, arity - offset>> {
+    return {s.begin() + offset, s.end()};
+}
+
+template <typename C>
+auto tail(C& s) {
+    return drop<1>(make_span(s));
+}
+
+/**
+ * A templated functor to obtain default values for
+ * unspecified elements of sparse array instances.
+ */
+template <typename T>
+struct default_factory {
+    T operator()() const {
+        return T();  // just use the default constructor
+    }
+};
+
+/**
+ * A functor representing the identity function.
+ */
+template <typename T>
+struct identity {
+    T operator()(T v) const {
+        return v;
+    }
+};
+
+/**
+ * A operation to be utilized by the sparse map when merging
+ * elements associated to different values.
+ */
+template <typename T>
+struct default_merge {
+    /**
+     * Merges two values a and b when merging spase maps.
+     */
+    T operator()(T a, T b) const {
+        default_factory<T> def;
+        // if a is the default => us b, else stick to a
+        return (a != def()) ? a : b;
+    }
+};
+
+/**
+ * Iterator type for `souffle::SparseArray`.
+ */
+template <typename SparseArray>
+struct SparseArrayIter {
+    using Node = typename SparseArray::Node;
+    using index_type = typename SparseArray::index_type;
+    using array_value_type = typename SparseArray::value_type;
+
+    using value_type = std::pair<index_type, array_value_type>;
+
+    SparseArrayIter() = default;  // default constructor -- creating an end-iterator
+    SparseArrayIter(const SparseArrayIter&) = default;
+    SparseArrayIter& operator=(const SparseArrayIter&) = default;
+
+    SparseArrayIter(const Node* node, value_type value) : node(node), value(std::move(value)) {}
+
+    SparseArrayIter(const Node* first, index_type firstOffset) : node(first), value(firstOffset, 0) {
+        // if the start is the end => we are done
+        if (!first) return;
+
+        // load the value
+        if (first->cell[0].value == array_value_type()) {
+            ++(*this);  // walk to first element
+        } else {
+            value.second = first->cell[0].value;
+        }
+    }
+
+    // the equality operator as required by the iterator concept
+    bool operator==(const SparseArrayIter& other) const {
+        // only equivalent if pointing to the end
+        return (node == nullptr && other.node == nullptr) ||
+               (node == other.node && value.first == other.value.first);
+    }
+
+    // the not-equality operator as required by the iterator concept
+    bool operator!=(const SparseArrayIter& other) const {
+        return !(*this == other);
+    }
+
+    // the deref operator as required by the iterator concept
+    const value_type& operator*() const {
+        return value;
+    }
+
+    // support for the pointer operator
+    const value_type* operator->() const {
+        return &value;
+    }
+
+    // the increment operator as required by the iterator concept
+    SparseArrayIter& operator++() {
+        assert(!isEnd());
+        // get current offset
+        index_type x = value.first & SparseArray::INDEX_MASK;
+
+        // go to next non-empty value in current node
+        do {
+            x++;
+        } while (x < SparseArray::NUM_CELLS && node->cell[x].value == array_value_type());
+
+        // check whether one has been found
+        if (x < SparseArray::NUM_CELLS) {
+            // update value and be done
+            value.first = (value.first & ~SparseArray::INDEX_MASK) | x;
+            value.second = node->cell[x].value;
+            return *this;  // done
+        }
+
+        // go to parent
+        node = node->parent;
+        int level = 1;
+
+        // get current index on this level
+        x = SparseArray::getIndex(brie_element_type(value.first), level);
+        x++;
+
+        while (level > 0 && node) {
+            // search for next child
+            while (x < SparseArray::NUM_CELLS) {
+                if (node->cell[x].ptr != nullptr) {
+                    break;
+                }
+                x++;
+            }
+
+            // pick next step
+            if (x < SparseArray::NUM_CELLS) {
+                // going down
+                node = node->cell[x].ptr;
+                value.first &= SparseArray::getLevelMask(level + 1);
+                value.first |= x << (SparseArray::BIT_PER_STEP * level);
+                level--;
+                x = 0;
+            } else {
+                // going up
+                node = node->parent;
+                level++;
+
+                // get current index on this level
+                x = SparseArray::getIndex(brie_element_type(value.first), level);
+                x++;  // go one step further
+            }
+        }
+
+        // check whether it is the end of range
+        if (node == nullptr) {
+            return *this;
+        }
+
+        // search the first value in this node
+        x = 0;
+        while (node->cell[x].value == array_value_type()) {
+            x++;
+        }
+
+        // update value
+        value.first |= x;
+        value.second = node->cell[x].value;
+
+        // done
+        return *this;
+    }
+
+    SparseArrayIter operator++(int) {
+        auto cpy = *this;
+        ++(*this);
+        return cpy;
+    }
+
+    // True if this iterator is passed the last element.
+    bool isEnd() const {
+        return node == nullptr;
+    }
+
+    // enables this iterator core to be printed (for debugging)
+    void print(std::ostream& out) const {
+        // `StreamUtil.h` defines an overload for `pair`, but we can't rely on it b/c
+        // it's disabled if `__EMBEDDED__` is defined.
+        out << "SparseArrayIter(" << node << " @ (" << value.first << ", " << value.second << "))";
+    }
+
+    friend std::ostream& operator<<(std::ostream& out, const SparseArrayIter& iter) {
+        iter.print(out);
+        return out;
+    }
+
+private:
+    // a pointer to the leaf node currently processed or null (end)
+    const Node* node{};
+
+    // the value currently pointed to
+    value_type value;
+};
+
+}  // namespace detail::brie
+
+using namespace detail::brie;
+
+/**
+ * A sparse array simulates an array associating to every element
+ * of uint32_t an element of a generic type T. Any non-defined element
+ * will be default-initialized utilizing the detail::brie::default_factory
+ * functor.
+ *
+ * Internally the array is organized as a balanced tree. The leaf
+ * level of the tree corresponds to the elements of the represented
+ * array. Inner nodes utilize individual bits of the indices to reference
+ * sub-trees. For efficiency reasons, only the minimal sub-tree required
+ * to cover all non-null / non-default values stored in the array is
+ * maintained. Furthermore, several levels of nodes are aggreated in a
+ * B-tree like fashion to inprove cache utilization and reduce the number
+ * of steps required for lookup and insert operations.
+ *
+ * @tparam T the type of the stored elements
+ * @tparam BITS the number of bits consumed per node-level
+ *              e.g. if it is set to 3, the resulting tree will be of a degree of
+ *              2^3=8, and thus 8 child-pointers will be stored in each inner node
+ *              and as many values will be stored in each leaf node.
+ * @tparam merge_op the functor to be utilized when merging the content of two
+ *              instances of this type.
+ * @tparam copy_op a functor to be applied to each stored value when copying an
+ *              instance of this array. For instance, this is utilized by the
+ *              trie implementation to create a clone of each sub-tree instead
+ *              of preserving the original pointer.
+ */
+template <typename T, unsigned BITS = 6, typename merge_op = default_merge<T>, typename copy_op = identity<T>>
+class SparseArray {
+    template <typename A>
+    friend struct detail::brie::SparseArrayIter;
+
+    using this_t = SparseArray<T, BITS, merge_op, copy_op>;
+    using key_type = uint64_t;
+
+    // some internal constants
+    static constexpr int BIT_PER_STEP = BITS;
+    static constexpr int NUM_CELLS = 1 << BIT_PER_STEP;
+    static constexpr key_type INDEX_MASK = NUM_CELLS - 1;
+
+public:
+    // the type utilized for indexing contained elements
+    using index_type = key_type;
+
+    // the type of value stored in this array
+    using value_type = T;
+
+    // the atomic view on stored values
+    using atomic_value_type = std::atomic<value_type>;
+
+private:
+    struct Node;
+
+    /**
+     * The value stored in a single cell of a inner
+     * or leaf node.
+     */
+    union Cell {
+        // an atomic view on the pointer referencing a nested level
+        std::atomic<Node*> aptr;
+
+        // a pointer to the nested level (unsynchronized operations)
+        Node* ptr{nullptr};
+
+        // an atomic view on the value stored in this cell (leaf node)
+        atomic_value_type avalue;
+
+        // the value stored in this cell (unsynchronized access, leaf node)
+        value_type value;
+    };
+
+    /**
+     * The node type of the internally maintained tree.
+     */
+    struct Node {
+        // a pointer to the parent node (for efficient iteration)
+        const Node* parent;
+        // the pointers to the child nodes (inner nodes) or the stored values (leaf nodes)
+        Cell cell[NUM_CELLS];
+    };
+
+    /**
+     * A struct describing all the information required by the container
+     * class to manage the wrapped up tree.
+     */
+    struct RootInfo {
+        // the root node of the tree
+        Node* root;
+        // the number of levels of the tree
+        uint32_t levels;
+        // the absolute offset of the theoretical first element in the tree
+        index_type offset;
+
+        // the first leaf node in the tree
+        Node* first;
+        // the absolute offset of the first element in the first leaf node
+        index_type firstOffset;
+    };
+
+    union {
+        RootInfo unsynced;         // for sequential operations
+        volatile RootInfo synced;  // for synchronized operations
+    };
+
+public:
+    /**
+     * A default constructor creating an empty sparse array.
+     */
+    SparseArray() : unsynced(RootInfo{nullptr, 0, 0, nullptr, std::numeric_limits<index_type>::max()}) {}
+
+    /**
+     * A copy constructor for sparse arrays. It creates a deep
+     * copy of the data structure maintained by the handed in
+     * array instance.
+     */
+    SparseArray(const SparseArray& other)
+            : unsynced(RootInfo{clone(other.unsynced.root, other.unsynced.levels), other.unsynced.levels,
+                      other.unsynced.offset, nullptr, other.unsynced.firstOffset}) {
+        if (unsynced.root) {
+            unsynced.root->parent = nullptr;
+            unsynced.first = findFirst(unsynced.root, unsynced.levels);
+        }
+    }
+
+    /**
+     * A r-value based copy constructor for sparse arrays. It
+     * takes over ownership of the structure maintained by the
+     * handed in array.
+     */
+    SparseArray(SparseArray&& other)
+            : unsynced(RootInfo{other.unsynced.root, other.unsynced.levels, other.unsynced.offset,
+                      other.unsynced.first, other.unsynced.firstOffset}) {
+        other.unsynced.root = nullptr;
+        other.unsynced.levels = 0;
+        other.unsynced.first = nullptr;
+    }
+
+    /**
+     * A destructor for sparse arrays clearing up the internally
+     * maintained data structure.
+     */
+    ~SparseArray() {
+        clean();
+    }
+
+    /**
+     * An assignment creating a deep copy of the handed in
+     * array structure (utilizing the copy functor provided
+     * as a template parameter).
+     */
+    SparseArray& operator=(const SparseArray& other) {
+        if (this == &other) return *this;
+
+        // clean this one
+        clean();
+
+        // copy content
+        unsynced.levels = other.unsynced.levels;
+        unsynced.root = clone(other.unsynced.root, unsynced.levels);
+        if (unsynced.root) {
+            unsynced.root->parent = nullptr;
+        }
+        unsynced.offset = other.unsynced.offset;
+        unsynced.first = (unsynced.root) ? findFirst(unsynced.root, unsynced.levels) : nullptr;
+        unsynced.firstOffset = other.unsynced.firstOffset;
+
+        // done
+        return *this;
+    }
+
+    /**
+     * An assignment operation taking over ownership
+     * from a r-value reference to a sparse array.
+     */
+    SparseArray& operator=(SparseArray&& other) {
+        // clean this one
+        clean();
+
+        // harvest content
+        unsynced.root = other.unsynced.root;
+        unsynced.levels = other.unsynced.levels;
+        unsynced.offset = other.unsynced.offset;
+        unsynced.first = other.unsynced.first;
+        unsynced.firstOffset = other.unsynced.firstOffset;
+
+        // reset other
+        other.unsynced.root = nullptr;
+        other.unsynced.levels = 0;
+        other.unsynced.first = nullptr;
+
+        // done
+        return *this;
+    }
+
+    /**
+     * Tests whether this sparse array is empty, thus it only
+     * contains default-values, or not.
+     */
+    bool empty() const {
+        return unsynced.root == nullptr;
+    }
+
+    /**
+     * Computes the number of non-empty elements within this
+     * sparse array.
+     */
+    std::size_t size() const {
+        // quick one for the empty map
+        if (empty()) return 0;
+
+        // count elements -- since maintaining is making inserts more expensive
+        std::size_t res = 0;
+        for (auto it = begin(); it != end(); ++it) {
+            ++res;
+        }
+        return res;
+    }
+
+private:
+    /**
+     * Computes the memory usage of the given sub-tree.
+     */
+    static std::size_t getMemoryUsage(const Node* node, int level) {
+        // support null-nodes
+        if (!node) return 0;
+
+        // add size of current node
+        std::size_t res = sizeof(Node);
+
+        // sum up memory usage of child nodes
+        if (level > 0) {
+            for (int i = 0; i < NUM_CELLS; i++) {
+                res += getMemoryUsage(node->cell[i].ptr, level - 1);
+            }
+        }
+
+        // done
+        return res;
+    }
+
+public:
+    /**
+     * Computes the total memory usage of this data structure.
+     */
+    std::size_t getMemoryUsage() const {
+        // the memory of the wrapper class
+        std::size_t res = sizeof(*this);
+
+        // add nodes
+        if (unsynced.root) {
+            res += getMemoryUsage(unsynced.root, unsynced.levels);
+        }
+
+        // done
+        return res;
+    }
+
+    /**
+     * Resets the content of this array to default values for each contained
+     * element.
+     */
+    void clear() {
+        clean();
+        unsynced.root = nullptr;
+        unsynced.levels = 0;
+        unsynced.first = nullptr;
+        unsynced.firstOffset = std::numeric_limits<index_type>::max();
+    }
+
+    /**
+     * A struct to be utilized as a local, temporal context by client code
+     * to speed up the execution of various operations (optional parameter).
+     */
+    struct op_context {
+        index_type lastIndex{0};
+        Node* lastNode{nullptr};
+        op_context() = default;
+    };
+
+private:
+    // ---------------------------------------------------------------------
+    //              Optimistic Locking of Root-Level Infos
+    // ---------------------------------------------------------------------
+
+    /**
+     * A struct to cover a snapshot of the root node state.
+     */
+    struct RootInfoSnapshot {
+        // the current pointer to a root node
+        Node* root;
+        // the current number of levels
+        uint32_t levels;
+        // the current offset of the first theoretical element
+        index_type offset;
+        // a version number for the optimistic locking
+        uintptr_t version;
+    };
+
+    /**
+     * Obtains the current version of the root.
+     */
+    uint64_t getRootVersion() const {
+        // here it is assumed that the load of a 64-bit word is atomic
+        return (uint64_t)synced.root;
+    }
+
+    /**
+     * Obtains a snapshot of the current root information.
+     */
+    RootInfoSnapshot getRootInfo() const {
+        RootInfoSnapshot res{};
+        do {
+            // first take the mod counter
+            do {
+                // if res.mod % 2 == 1 .. there is an update in progress
+                res.version = getRootVersion();
+            } while (res.version % 2);
+
+            // then the rest
+            res.root = synced.root;
+            res.levels = synced.levels;
+            res.offset = synced.offset;
+
+            // check consistency of obtained data (optimistic locking)
+        } while (res.version != getRootVersion());
+
+        // got a consistent snapshot
+        return res;
+    }
+
+    /**
+     * Updates the current root information based on the handed in modified
+     * snapshot instance if the version number of the snapshot still corresponds
+     * to the current version. Otherwise a concurrent update took place and the
+     * operation is aborted.
+     *
+     * @param info the updated information to be assigned to the active root-info data
+     * @return true if successfully updated, false if aborted
+     */
+    bool tryUpdateRootInfo(const RootInfoSnapshot& info) {
+        // check mod counter
+        uintptr_t version = info.version;
+
+        // update root to invalid pointer (ending with 1)
+        if (!__sync_bool_compare_and_swap(&synced.root, (Node*)version, (Node*)(version + 1))) {
+            return false;
+        }
+
+        // conduct update
+        synced.levels = info.levels;
+        synced.offset = info.offset;
+
+        // update root (and thus the version to enable future retrievals)
+        __sync_synchronize();
+        synced.root = info.root;
+
+        // done
+        return true;
+    }
+
+    /**
+     * A struct summarizing the state of the first node reference.
+     */
+    struct FirstInfoSnapshot {
+        // the pointer to the first node
+        Node* node;
+        // the offset of the first node
+        index_type offset;
+        // the version number of the first node (for the optimistic locking)
+        uintptr_t version;
+    };
+
+    /**
+     * Obtains the current version number of the first node information.
+     */
+    uint64_t getFirstVersion() const {
+        // here it is assumed that the load of a 64-bit word is atomic
+        return (uint64_t)synced.first;
+    }
+
+    /**
+     * Obtains a snapshot of the current first-node information.
+     */
+    FirstInfoSnapshot getFirstInfo() const {
+        FirstInfoSnapshot res{};
+        do {
+            // first take the version
+            do {
+                res.version = getFirstVersion();
+            } while (res.version % 2);
+
+            // collect the values
+            res.node = synced.first;
+            res.offset = synced.firstOffset;
+
+        } while (res.version != getFirstVersion());
+
+        // we got a consistent snapshot
+        return res;
+    }
+
+    /**
+     * Updates the information stored regarding the first node in a
+     * concurrent setting utilizing a optimistic locking approach.
+     * This is identical to the approach utilized for the root info.
+     */
+    bool tryUpdateFirstInfo(const FirstInfoSnapshot& info) {
+        // check mod counter
+        uintptr_t version = info.version;
+
+        // temporary update first pointer to point to uneven value (lock-out)
+        if (!__sync_bool_compare_and_swap(&synced.first, (Node*)version, (Node*)(version + 1))) {
+            return false;
+        }
+
+        // conduct update
+        synced.firstOffset = info.offset;
+
+        // update node pointer (and thus the version number)
+        __sync_synchronize();
+        synced.first = info.node;  // must be last (and atomic)
+
+        // done
+        return true;
+    }
+
+public:
+    /**
+     * Obtains a mutable reference to the value addressed by the given index.
+     *
+     * @param i the index of the element to be addressed
+     * @return a mutable reference to the corresponding element
+     */
+    value_type& get(index_type i) {
+        op_context ctxt;
+        return get(i, ctxt);
+    }
+
+    /**
+     * Obtains a mutable reference to the value addressed by the given index.
+     *
+     * @param i the index of the element to be addressed
+     * @param ctxt a operation context to exploit state-less temporal locality
+     * @return a mutable reference to the corresponding element
+     */
+    value_type& get(index_type i, op_context& ctxt) {
+        return getLeaf(i, ctxt).value;
+    }
+
+    /**
+     * Obtains a mutable reference to the atomic value addressed by the given index.
+     *
+     * @param i the index of the element to be addressed
+     * @return a mutable reference to the corresponding element
+     */
+    atomic_value_type& getAtomic(index_type i) {
+        op_context ctxt;
+        return getAtomic(i, ctxt);
+    }
+
+    /**
+     * Obtains a mutable reference to the atomic value addressed by the given index.
+     *
+     * @param i the index of the element to be addressed
+     * @param ctxt a operation context to exploit state-less temporal locality
+     * @return a mutable reference to the corresponding element
+     */
+    atomic_value_type& getAtomic(index_type i, op_context& ctxt) {
+        return getLeaf(i, ctxt).avalue;
+    }
+
+private:
+    /**
+     * An internal function capable of navigating to a given leaf node entry.
+     * If the cell does not exist yet it will be created as a side-effect.
+     *
+     * @param i the index of the requested cell
+     * @param ctxt a operation context to exploit state-less temporal locality
+     * @return a reference to the requested cell
+     */
+    inline Cell& getLeaf(index_type i, op_context& ctxt) {
+        // check context
+        if (ctxt.lastNode && (ctxt.lastIndex == (i & ~INDEX_MASK))) {
+            // return reference to referenced
+            return ctxt.lastNode->cell[i & INDEX_MASK];
+        }
+
+        // get snapshot of root
+        auto info = getRootInfo();
+
+        // check for emptiness
+        if (info.root == nullptr) {
+            // build new root node
+            info.root = newNode();
+
+            // initialize the new node
+            info.root->parent = nullptr;
+            info.offset = i & ~(INDEX_MASK);
+
+            // try updating root information atomically
+            if (tryUpdateRootInfo(info)) {
+                // success -- finish get call
+
+                // update first
+                auto firstInfo = getFirstInfo();
+                while (info.offset < firstInfo.offset) {
+                    firstInfo.node = info.root;
+                    firstInfo.offset = info.offset;
+                    if (!tryUpdateFirstInfo(firstInfo)) {
+                        // there was some concurrent update => check again
+                        firstInfo = getFirstInfo();
+                    }
+                }
+
+                // return reference to proper cell
+                return info.root->cell[i & INDEX_MASK];
+            }
+
+            // somebody else was faster => use standard insertion procedure
+            delete info.root;
+
+            // retrieve new root info
+            info = getRootInfo();
+
+            // make sure there is a root
+            assert(info.root);
+        }
+
+        // for all other inserts
+        //   - check boundary
+        //   - navigate to node
+        //   - insert value
+
+        // check boundaries
+        while (!inBoundaries(i, info.levels, info.offset)) {
+            // boundaries need to be expanded by growing upwards
+            raiseLevel(info);  // try raising level unless someone else did already
+            // update root info
+            info = getRootInfo();
+        }
+
+        // navigate to node
+        Node* node = info.root;
+        unsigned level = info.levels;
+        while (level != 0) {
+            // get X coordinate
+            auto x = getIndex(brie_element_type(i), level);
+
+            // decrease level counter
+            --level;
+
+            // check next node
+            std::atomic<Node*>& aNext = node->cell[x].aptr;
+            Node* next = aNext;
+            if (!next) {
+                // create new sub-tree
+                Node* newNext = newNode();
+                newNext->parent = node;
+
+                // try to update next
+                if (!aNext.compare_exchange_strong(next, newNext)) {
+                    // some other thread was faster => use updated next
+                    delete newNext;
+                } else {
+                    // the locally created next is the new next
+                    next = newNext;
+
+                    // update first
+                    if (level == 0) {
+                        // compute offset of this node
+                        auto off = i & ~INDEX_MASK;
+
+                        // fast over-approximation of whether a update is necessary
+                        if (off < unsynced.firstOffset) {
+                            // update first reference if this one is the smallest
+                            auto first_info = getFirstInfo();
+                            while (off < first_info.offset) {
+                                first_info.node = next;
+                                first_info.offset = off;
+                                if (!tryUpdateFirstInfo(first_info)) {
+                                    // there was some concurrent update => check again
+                                    first_info = getFirstInfo();
+                                }
+                            }
+                        }
+                    }
+                }
+
+                // now next should be defined
+                assert(next);
+            }
+
+            // continue one level below
+            node = next;
+        }
+
+        // update context
+        ctxt.lastIndex = (i & ~INDEX_MASK);
+        ctxt.lastNode = node;
+
+        // return reference to cell
+        return node->cell[i & INDEX_MASK];
+    }
+
+public:
+    /**
+     * Updates the value stored in cell i by the given value.
+     */
+    void update(index_type i, const value_type& val) {
+        op_context ctxt;
+        update(i, val, ctxt);
+    }
+
+    /**
+     * Updates the value stored in cell i by the given value. A operation
+     * context can be provided for exploiting temporal locality.
+     */
+    void update(index_type i, const value_type& val, op_context& ctxt) {
+        get(i, ctxt) = val;
+    }
+
+    /**
+     * Obtains the value associated to index i -- which might be
+     * the default value of the covered type if the value hasn't been
+     * defined previously.
+     */
+    value_type operator[](index_type i) const {
+        return lookup(i);
+    }
+
+    /**
+     * Obtains the value associated to index i -- which might be
+     * the default value of the covered type if the value hasn't been
+     * defined previously.
+     */
+    value_type lookup(index_type i) const {
+        op_context ctxt;
+        return lookup(i, ctxt);
+    }
+
+    /**
+     * Obtains the value associated to index i -- which might be
+     * the default value of the covered type if the value hasn't been
+     * defined previously. A operation context can be provided for
+     * exploiting temporal locality.
+     */
+    value_type lookup(index_type i, op_context& ctxt) const {
+        // check whether it is empty
+        if (!unsynced.root) return default_factory<value_type>()();
+
+        // check boundaries
+        if (!inBoundaries(i)) return default_factory<value_type>()();
+
+        // check context
+        if (ctxt.lastNode && ctxt.lastIndex == (i & ~INDEX_MASK)) {
+            return ctxt.lastNode->cell[i & INDEX_MASK].value;
+        }
+
+        // navigate to value
+        Node* node = unsynced.root;
+        unsigned level = unsynced.levels;
+        while (level != 0) {
+            // get X coordinate
+            auto x = getIndex(brie_element_type(i), level);
+
+            // decrease level counter
+            --level;
+
+            // check next node
+            Node* next = node->cell[x].ptr;
+
+            // check next step
+            if (!next) return default_factory<value_type>()();
+
+            // continue one level below
+            node = next;
+        }
+
+        // remember context
+        ctxt.lastIndex = (i & ~INDEX_MASK);
+        ctxt.lastNode = node;
+
+        // return reference to cell
+        return node->cell[i & INDEX_MASK].value;
+    }
+
+private:
+    /**
+     * A static operation utilized internally for merging sub-trees recursively.
+     *
+     * @param parent the parent node of the current merge operation
+     * @param trg a reference to the pointer the cloned node should be stored to
+     * @param src the node to be cloned
+     * @param levels the height of the cloned node
+     */
+    static void merge(const Node* parent, Node*& trg, const Node* src, int levels) {
+        // if other side is null => done
+        if (src == nullptr) {
+            return;
+        }
+
+        // if the trg sub-tree is empty, clone the corresponding branch
+        if (trg == nullptr) {
+            trg = clone(src, levels);
+            if (trg != nullptr) {
+                trg->parent = parent;
+            }
+            return;  // done
+        }
+
+        // otherwise merge recursively
+
+        // the leaf-node step
+        if (levels == 0) {
+            merge_op merg;
+            for (int i = 0; i < NUM_CELLS; ++i) {
+                trg->cell[i].value = merg(trg->cell[i].value, src->cell[i].value);
+            }
+            return;
+        }
+
+        // the recursive step
+        for (int i = 0; i < NUM_CELLS; ++i) {
+            merge(trg, trg->cell[i].ptr, src->cell[i].ptr, levels - 1);
+        }
+    }
+
+public:
+    /**
+     * Adds all the values stored in the given array to this array.
+     */
+    void addAll(const SparseArray& other) {
+        // skip if other is empty
+        if (other.empty()) {
+            return;
+        }
+
+        // special case: emptiness
+        if (empty()) {
+            // use assignment operator
+            *this = other;
+            return;
+        }
+
+        // adjust levels
+        while (unsynced.levels < other.unsynced.levels || !inBoundaries(other.unsynced.offset)) {
+            raiseLevel();
+        }
+
+        // navigate to root node equivalent of the other node in this tree
+        auto level = unsynced.levels;
+        Node** node = &unsynced.root;
+        while (level > other.unsynced.levels) {
+            // get X coordinate
+            auto x = getIndex(brie_element_type(other.unsynced.offset), level);
+
+            // decrease level counter
+            --level;
+
+            // check next node
+            Node*& next = (*node)->cell[x].ptr;
+            if (!next) {
+                // create new sub-tree
+                next = newNode();
+                next->parent = *node;
+            }
+
+            // continue one level below
+            node = &next;
+        }
+
+        // merge sub-branches from here
+        merge((*node)->parent, *node, other.unsynced.root, level);
+
+        // update first
+        if (unsynced.firstOffset > other.unsynced.firstOffset) {
+            unsynced.first = findFirst(*node, level);
+            unsynced.firstOffset = other.unsynced.firstOffset;
+        }
+    }
+
+    // ---------------------------------------------------------------------
+    //                           Iterator
+    // ---------------------------------------------------------------------
+
+    using iterator = SparseArrayIter<this_t>;
+
+    /**
+     * Obtains an iterator referencing the first non-default element or end in
+     * case there are no such elements.
+     */
+    iterator begin() const {
+        return iterator(unsynced.first, unsynced.firstOffset);
+    }
+
+    /**
+     * An iterator referencing the position after the last non-default element.
+     */
+    iterator end() const {
+        return iterator();
+    }
+
+    /**
+     * An operation to obtain an iterator referencing an element addressed by the
+     * given index. If the corresponding element is a non-default value, a corresponding
+     * iterator will be returned. Otherwise end() will be returned.
+     */
+    iterator find(index_type i) const {
+        op_context ctxt;
+        return find(i, ctxt);
+    }
+
+    /**
+     * An operation to obtain an iterator referencing an element addressed by the
+     * given index. If the corresponding element is a non-default value, a corresponding
+     * iterator will be returned. Otherwise end() will be returned. A operation context
+     * can be provided for exploiting temporal locality.
+     */
+    iterator find(index_type i, op_context& ctxt) const {
+        // check whether it is empty
+        if (!unsynced.root) return end();
+
+        // check boundaries
+        if (!inBoundaries(i)) return end();
+
+        // check context
+        if (ctxt.lastNode && ctxt.lastIndex == (i & ~INDEX_MASK)) {
+            Node* node = ctxt.lastNode;
+
+            // check whether there is a proper entry
+            value_type value = node->cell[i & INDEX_MASK].value;
+            if (value == value_type{}) {
+                return end();
+            }
+            // return iterator pointing to value
+            return iterator(node, std::make_pair(i, value));
+        }
+
+        // navigate to value
+        Node* node = unsynced.root;
+        unsigned level = unsynced.levels;
+        while (level != 0) {
+            // get X coordinate
+            auto x = getIndex(i, level);
+
+            // decrease level counter
+            --level;
+
+            // check next node
+            Node* next = node->cell[x].ptr;
+
+            // check next step
+            if (!next) return end();
+
+            // continue one level below
+            node = next;
+        }
+
+        // register in context
+        ctxt.lastNode = node;
+        ctxt.lastIndex = (i & ~INDEX_MASK);
+
+        // check whether there is a proper entry
+        value_type value = node->cell[i & INDEX_MASK].value;
+        if (value == value_type{}) {
+            return end();
+        }
+
+        // return iterator pointing to cell
+        return iterator(node, std::make_pair(i, value));
+    }
+
+    /**
+     * An operation obtaining the smallest non-default element such that it's index is >=
+     * the given index.
+     */
+    iterator lowerBound(index_type i) const {
+        op_context ctxt;
+        return lowerBound(i, ctxt);
+    }
+
+    /**
+     * An operation obtaining the smallest non-default element such that it's index is >=
+     * the given index. A operation context can be provided for exploiting temporal locality.
+     */
+    iterator lowerBound(index_type i, op_context&) const {
+        // check whether it is empty
+        if (!unsynced.root) return end();
+
+        // check boundaries
+        if (!inBoundaries(i)) {
+            // if it is on the lower end, return minimum result
+            if (i < unsynced.offset) {
+                const auto& value = unsynced.first->cell[0].value;
+                auto res = iterator(unsynced.first, std::make_pair(unsynced.offset, value));
+                if (value == value_type()) {
+                    ++res;
+                }
+                return res;
+            }
+            // otherwise it is on the high end, return end iterator
+            return end();
+        }
+
+        // navigate to value
+        Node* node = unsynced.root;
+        unsigned level = unsynced.levels;
+        while (true) {
+            // get X coordinate
+            auto x = getIndex(brie_element_type(i), level);
+
+            // check next node
+            Node* next = node->cell[x].ptr;
+
+            // check next step
+            if (!next) {
+                if (x == NUM_CELLS - 1) {
+                    ++level;
+                    node = const_cast<Node*>(node->parent);
+                    if (!node) return end();
+                }
+
+                // continue search
+                i = i & getLevelMask(level);
+
+                // find next higher value
+                i += 1ull << (BITS * level);
+
+            } else {
+                if (level == 0) {
+                    // found boundary
+                    return iterator(node, std::make_pair(i, node->cell[x].value));
+                }
+
+                // decrease level counter
+                --level;
+
+                // continue one level below
+                node = next;
+            }
+        }
+    }
+
+    /**
+     * An operation obtaining the smallest non-default element such that it's index is greater
+     * the given index.
+     */
+    iterator upperBound(index_type i) const {
+        op_context ctxt;
+        return upperBound(i, ctxt);
+    }
+
+    /**
+     * An operation obtaining the smallest non-default element such that it's index is greater
+     * the given index. A operation context can be provided for exploiting temporal locality.
+     */
+    iterator upperBound(index_type i, op_context& ctxt) const {
+        if (i == std::numeric_limits<index_type>::max()) {
+            return end();
+        }
+        return lowerBound(i + 1, ctxt);
+    }
+
+private:
+    /**
+     * An internal debug utility printing the internal structure of this sparse array to the given output
+     * stream.
+     */
+    void dump(bool detailed, std::ostream& out, const Node& node, int level, index_type offset,
+            int indent = 0) const {
+        auto x = getIndex(offset, level + 1);
+        out << times("\t", indent) << x << ": Node " << &node << " on level " << level
+            << " parent: " << node.parent << " -- range: " << offset << " - "
+            << (offset + ~getLevelMask(level + 1)) << "\n";
+
+        if (level == 0) {
+            for (int i = 0; i < NUM_CELLS; i++) {
+                if (detailed || node.cell[i].value != value_type()) {
+                    out << times("\t", indent + 1) << i << ": [" << (offset + i) << "] " << node.cell[i].value
+                        << "\n";
+                }
+            }
+        } else {
+            for (int i = 0; i < NUM_CELLS; i++) {
+                if (node.cell[i].ptr) {
+                    dump(detailed, out, *node.cell[i].ptr, level - 1,
+                            offset + (i * (index_type(1) << (level * BIT_PER_STEP))), indent + 1);
+                } else if (detailed) {
+                    auto low = offset + (i * (1 << (level * BIT_PER_STEP)));
+                    auto hig = low + ~getLevelMask(level);
+                    out << times("\t", indent + 1) << i << ": empty range " << low << " - " << hig << "\n";
+                }
+            }
+        }
+        out << "\n";
+    }
+
+public:
+    /**
+     * A debug utility printing the internal structure of this sparse array to the given output stream.
+     */
+    void dump(bool detail = false, std::ostream& out = std::cout) const {
+        if (!unsynced.root) {
+            out << " - empty - \n";
+            return;
+        }
+        out << "root:  " << unsynced.root << "\n";
+        out << "offset: " << unsynced.offset << "\n";
+        out << "first: " << unsynced.first << "\n";
+        out << "fist offset: " << unsynced.firstOffset << "\n";
+        dump(detail, out, *unsynced.root, unsynced.levels, unsynced.offset);
+    }
+
+private:
+    // --------------------------------------------------------------------------
+    //                                 Utilities
+    // --------------------------------------------------------------------------
+
+    /**
+     * Creates new nodes and initializes them with 0.
+     */
+    static Node* newNode() {
+        return new Node();
+    }
+
+    /**
+     * Destroys a node and all its sub-nodes recursively.
+     */
+    static void freeNodes(Node* node, int level) {
+        if (!node) return;
+        if (level != 0) {
+            for (int i = 0; i < NUM_CELLS; i++) {
+                freeNodes(node->cell[i].ptr, level - 1);
+            }
+        }
+        delete node;
+    }
+
+    /**
+     * Conducts a cleanup of the internal tree structure.
+     */
+    void clean() {
+        freeNodes(unsynced.root, unsynced.levels);
+        unsynced.root = nullptr;
+        unsynced.levels = 0;
+    }
+
+    /**
+     * Clones the given node and all its sub-nodes.
+     */
+    static Node* clone(const Node* node, int level) {
+        // support null-pointers
+        if (node == nullptr) {
+            return nullptr;
+        }
+
+        // create a clone
+        auto* res = new Node();
+
+        // handle leaf level
+        if (level == 0) {
+            copy_op copy;
+            for (int i = 0; i < NUM_CELLS; i++) {
+                res->cell[i].value = copy(node->cell[i].value);
+            }
+            return res;
+        }
+
+        // for inner nodes clone each child
+        for (int i = 0; i < NUM_CELLS; i++) {
+            auto cur = clone(node->cell[i].ptr, level - 1);
+            if (cur != nullptr) {
+                cur->parent = res;
+            }
+            res->cell[i].ptr = cur;
+        }
+
+        // done
+        return res;
+    }
+
+    /**
+     * Obtains the left-most leaf-node of the tree rooted by the given node
+     * with the given level.
+     */
+    static Node* findFirst(Node* node, int level) {
+        while (level > 0) {
+            bool found = false;
+            for (int i = 0; i < NUM_CELLS; i++) {
+                Node* cur = node->cell[i].ptr;
+                if (cur) {
+                    node = cur;
+                    --level;
+                    found = true;
+                    break;
+                }
+            }
+            assert(found && "No first node!");
+        }
+
+        return node;
+    }
+
+    /**
+     * Raises the level of this tree by one level. It does so by introducing
+     * a new root node and inserting the current root node as a child node.
+     */
+    void raiseLevel() {
+        // something went wrong when we pass that line
+        assert(unsynced.levels < (sizeof(index_type) * 8 / BITS) + 1);
+
+        // create new root
+        Node* node = newNode();
+        node->parent = nullptr;
+
+        // insert existing root as child
+        auto x = getIndex(brie_element_type(unsynced.offset), unsynced.levels + 1);
+        node->cell[x].ptr = unsynced.root;
+
+        // swap the root
+        unsynced.root->parent = node;
+
+        // update root
+        unsynced.root = node;
+        ++unsynced.levels;
+
+        // update offset be removing additional bits
+        unsynced.offset &= getLevelMask(unsynced.levels + 1);
+    }
+
+    /**
+     * Attempts to raise the height of this tree based on the given root node
+     * information and updates the root-info snapshot correspondingly.
+     */
+    void raiseLevel(RootInfoSnapshot& info) {
+        // something went wrong when we pass that line
+        assert(info.levels < (sizeof(index_type) * 8 / BITS) + 1);
+
+        // create new root
+        Node* newRoot = newNode();
+        newRoot->parent = nullptr;
+
+        // insert existing root as child
+        auto x = getIndex(brie_element_type(info.offset), info.levels + 1);
+        newRoot->cell[x].ptr = info.root;
+
+        // exchange the root in the info struct
+        auto oldRoot = info.root;
+        info.root = newRoot;
+
+        // update level counter
+        ++info.levels;
+
+        // update offset
+        info.offset &= getLevelMask(info.levels + 1);
+
+        // try exchanging root info
+        if (tryUpdateRootInfo(info)) {
+            // success => final step, update parent of old root
+            oldRoot->parent = info.root;
+        } else {
+            // throw away temporary new node
+            delete newRoot;
+        }
+    }
+
+    /**
+     * Tests whether the given index is covered by the boundaries defined
+     * by the hight and offset of the internally maintained tree.
+     */
+    bool inBoundaries(index_type a) const {
+        return inBoundaries(a, unsynced.levels, unsynced.offset);
+    }
+
+    /**
+     * Tests whether the given index is within the boundaries defined by the
+     * given tree hight and offset.
+     */
+    static bool inBoundaries(index_type a, uint32_t levels, index_type offset) {
+        auto mask = getLevelMask(levels + 1);
+        return (a & mask) == offset;
+    }
+
+    /**
+     * Obtains the index within the arrays of cells of a given index on a given
+     * level of the internally maintained tree.
+     */
+    static index_type getIndex(brie_element_type a, unsigned level) {
+        return (a & (INDEX_MASK << (level * BIT_PER_STEP))) >> (level * BIT_PER_STEP);
+    }
+
+    /**
+     * Computes the bit-mask to be applicable to obtain the offset of a node on a
+     * given tree level.
+     */
+    static index_type getLevelMask(unsigned level) {
+        if (level > (sizeof(index_type) * 8 / BITS)) return 0;
+        return (~(index_type(0)) << (level * BIT_PER_STEP));
+    }
+};
+
+namespace detail::brie {
+
+/**
+ * Iterator type for `souffle::SparseArray`. It enumerates the indices set to 1.
+ */
+template <typename SparseBitMap>
+class SparseBitMapIter {
+    using value_t = typename SparseBitMap::value_t;
+    using value_type = typename SparseBitMap::index_type;
+    using data_store_t = typename SparseBitMap::data_store_t;
+    using nested_iterator = typename data_store_t::iterator;
+
+    // the iterator through the underlying sparse data structure
+    nested_iterator iter;
+
+    // the currently consumed mask
+    uint64_t mask = 0;
+
+    // the value currently pointed to
+    value_type value{};
+
+public:
+    SparseBitMapIter() = default;  // default constructor -- creating an end-iterator
+    SparseBitMapIter(const SparseBitMapIter&) = default;
+    SparseBitMapIter& operator=(const SparseBitMapIter&) = default;
+
+    SparseBitMapIter(const nested_iterator& iter)
+            : iter(iter), mask(SparseBitMap::toMask(iter->second)),
+              value(iter->first << SparseBitMap::LEAF_INDEX_WIDTH) {
+        moveToNextInMask();
+    }
+
+    SparseBitMapIter(const nested_iterator& iter, uint64_t m, value_type value)
+            : iter(iter), mask(m), value(value) {}
+
+    // the equality operator as required by the iterator concept
+    bool operator==(const SparseBitMapIter& other) const {
+        // only equivalent if pointing to the end
+        return iter == other.iter && mask == other.mask;
+    }
+
+    // the not-equality operator as required by the iterator concept
+    bool operator!=(const SparseBitMapIter& other) const {
+        return !(*this == other);
+    }
+
+    // the deref operator as required by the iterator concept
+    const value_type& operator*() const {
+        return value;
+    }
+
+    // support for the pointer operator
+    const value_type* operator->() const {
+        return &value;
+    }
+
+    // the increment operator as required by the iterator concept
+    SparseBitMapIter& operator++() {
+        // progress in current mask
+        if (moveToNextInMask()) return *this;
+
+        // go to next entry
+        ++iter;
+
+        // update value
+        if (!iter.isEnd()) {
+            value = iter->first << SparseBitMap::LEAF_INDEX_WIDTH;
+            mask = SparseBitMap::toMask(iter->second);
+            moveToNextInMask();
+        }
+
+        // done
+        return *this;
+    }
+
+    SparseBitMapIter operator++(int) {
+        auto cpy = *this;
+        ++(*this);
+        return cpy;
+    }
+
+    bool isEnd() const {
+        return iter.isEnd();
+    }
+
+    void print(std::ostream& out) const {
+        out << "SparseBitMapIter(" << iter << " -> " << std::bitset<64>(mask) << " @ " << value << ")";
+    }
+
+    // enables this iterator core to be printed (for debugging)
+    friend std::ostream& operator<<(std::ostream& out, const SparseBitMapIter& iter) {
+        iter.print(out);
+        return out;
+    }
+
+private:
+    bool moveToNextInMask() {
+        // check if there is something left
+        if (mask == 0) return false;
+
+        // get position of leading 1
+        auto pos = __builtin_ctzll(mask);
+
+        // consume this bit
+        mask &= ~(1llu << pos);
+
+        // update value
+        value &= ~SparseBitMap::LEAF_INDEX_MASK;
+        value |= pos;
+
+        // done
+        return true;
+    }
+};
+
+}  // namespace detail::brie
+
+/**
+ * A sparse bit-map is a bit map virtually assigning a bit value to every value if the
+ * uint32_t domain. However, only 1-bits are stored utilizing a nested sparse array
+ * structure.
+ *
+ * @tparam BITS similar to the BITS parameter of the sparse array type
+ */
+template <unsigned BITS = 4>
+class SparseBitMap {
+    template <typename A>
+    friend class detail::brie::SparseBitMapIter;
+
+    using this_t = SparseBitMap<BITS>;
+
+    // the element type stored in the nested sparse array
+    using value_t = uint64_t;
+
+    // define the bit-level merge operation
+    struct merge_op {
+        value_t operator()(value_t a, value_t b) const {
+            return a | b;  // merging bit masks => bitwise or operation
+        }
+    };
+
+    // the type of the internal data store
+    using data_store_t = SparseArray<value_t, BITS, merge_op>;
+    using atomic_value_t = typename data_store_t::atomic_value_type;
+
+    // some constants for manipulating stored values
+    static constexpr std::size_t BITS_PER_ENTRY = sizeof(value_t) * CHAR_BIT;
+    static constexpr std::size_t LEAF_INDEX_WIDTH = __builtin_ctz(BITS_PER_ENTRY);
+    static constexpr uint64_t LEAF_INDEX_MASK = BITS_PER_ENTRY - 1;
+
+    static uint64_t toMask(const value_t& value) {
+        static_assert(sizeof(value_t) == sizeof(uint64_t), "Fixed for 64-bit compiler.");
+        return reinterpret_cast<const uint64_t&>(value);
+    }
+
+public:
+    // the type to address individual entries
+    using index_type = typename data_store_t::index_type;
+
+private:
+    // it utilizes a sparse map to store its data
+    data_store_t store;
+
+public:
+    // a simple default constructor
+    SparseBitMap() = default;
+
+    // a default copy constructor
+    SparseBitMap(const SparseBitMap&) = default;
+
+    // a default r-value copy constructor
+    SparseBitMap(SparseBitMap&&) = default;
+
+    // a default assignment operator
+    SparseBitMap& operator=(const SparseBitMap&) = default;
+
+    // a default r-value assignment operator
+    SparseBitMap& operator=(SparseBitMap&&) = default;
+
+    // checks whether this bit-map is empty -- thus it does not have any 1-entries
+    bool empty() const {
+        return store.empty();
+    }
+
+    // the type utilized for recording context information for exploiting temporal locality
+    using op_context = typename data_store_t::op_context;
+
+    /**
+     * Sets the bit addressed by i to 1.
+     */
+    bool set(index_type i) {
+        op_context ctxt;
+        return set(i, ctxt);
+    }
+
+    /**
+     * Sets the bit addressed by i to 1. A context for exploiting temporal locality
+     * can be provided.
+     */
+    bool set(index_type i, op_context& ctxt) {
+        atomic_value_t& val = store.getAtomic(i >> LEAF_INDEX_WIDTH, ctxt);
+        value_t bit = (1ull << (i & LEAF_INDEX_MASK));
+
+#ifdef __GNUC__
+#if __GNUC__ >= 7
+        // In GCC >= 7 the usage of fetch_or causes a bug that needs further investigation
+        // For now, this two-instruction based implementation provides a fix that does
+        // not sacrifice too much performance.
+
+        while (true) {
+            auto order = std::memory_order::memory_order_relaxed;
+
+            // load current value
+            value_t old = val.load(order);
+
+            // if bit is already set => we are done
+            if (old & bit) return false;
+
+            // set the bit, if failed, repeat
+            if (!val.compare_exchange_strong(old, old | bit, order, order)) continue;
+
+            // it worked, new bit added
+            return true;
+        }
+
+#endif
+#endif
+
+        value_t old = val.fetch_or(bit, std::memory_order::memory_order_relaxed);
+        return (old & bit) == 0u;
+    }
+
+    /**
+     * Determines the whether the bit addressed by i is set or not.
+     */
+    bool test(index_type i) const {
+        op_context ctxt;
+        return test(i, ctxt);
+    }
+
+    /**
+     * Determines the whether the bit addressed by i is set or not. A context for
+     * exploiting temporal locality can be provided.
+     */
+    bool test(index_type i, op_context& ctxt) const {
+        value_t bit = (1ull << (i & LEAF_INDEX_MASK));
+        return store.lookup(i >> LEAF_INDEX_WIDTH, ctxt) & bit;
+    }
+
+    /**
+     * Determines the whether the bit addressed by i is set or not.
+     */
+    bool operator[](index_type i) const {
+        return test(i);
+    }
+
+    /**
+     * Resets all contained bits to 0.
+     */
+    void clear() {
+        store.clear();
+    }
+
+    /**
+     * Determines the number of bits set.
+     */
+    std::size_t size() const {
+        // this is computed on demand to keep the set operation simple.
+        std::size_t res = 0;
+        for (const auto& cur : store) {
+            res += __builtin_popcountll(cur.second);
+        }
+        return res;
+    }
+
+    /**
+     * Computes the total memory usage of this data structure.
+     */
+    std::size_t getMemoryUsage() const {
+        // compute the total memory usage
+        return sizeof(*this) - sizeof(data_store_t) + store.getMemoryUsage();
+    }
+
+    /**
+     * Sets all bits set in other to 1 within this bit map.
+     */
+    void addAll(const SparseBitMap& other) {
+        // nothing to do if it is a self-assignment
+        if (this == &other) return;
+
+        // merge the sparse store
+        store.addAll(other.store);
+    }
+
+    // ---------------------------------------------------------------------
+    //                           Iterator
+    // ---------------------------------------------------------------------
+
+    using iterator = SparseBitMapIter<this_t>;
+
+    /**
+     * Obtains an iterator pointing to the first index set to 1. If there
+     * is no such bit, end() will be returned.
+     */
+    iterator begin() const {
+        auto it = store.begin();
+        if (it.isEnd()) return end();
+        return iterator(it);
+    }
+
+    /**
+     * Returns an iterator referencing the position after the last set bit.
+     */
+    iterator end() const {
+        return iterator();
+    }
+
+    /**
+     * Obtains an iterator referencing the position i if the corresponding
+     * bit is set, end() otherwise.
+     */
+    iterator find(index_type i) const {
+        op_context ctxt;
+        return find(i, ctxt);
+    }
+
+    /**
+     * Obtains an iterator referencing the position i if the corresponding
+     * bit is set, end() otherwise. An operation context can be provided
+     * to exploit temporal locality.
+     */
+    iterator find(index_type i, op_context& ctxt) const {
+        // check prefix part
+        auto it = store.find(i >> LEAF_INDEX_WIDTH, ctxt);
+        if (it.isEnd()) return end();
+
+        // check bit-set part
+        uint64_t mask = toMask(it->second);
+        if (!(mask & (1llu << (i & LEAF_INDEX_MASK)))) return end();
+
+        // OK, it is there => create iterator
+        mask &= ((1ull << (i & LEAF_INDEX_MASK)) - 1);  // remove all bits before pos i
+        return iterator(it, mask, i);
+    }
+
+    /**
+     * Locates an iterator to the first element in this sparse bit map not less
+     * than the given index.
+     */
+    iterator lower_bound(index_type i) const {
+        auto it = store.lowerBound(i >> LEAF_INDEX_WIDTH);
+        if (it.isEnd()) return end();
+
+        // check bit-set part
+        uint64_t mask = toMask(it->second);
+
+        // if there is no bit remaining in this mask, check next mask.
+        if (!(mask & ((~uint64_t(0)) << (i & LEAF_INDEX_MASK)))) {
+            index_type next = ((i >> LEAF_INDEX_WIDTH) + 1) << LEAF_INDEX_WIDTH;
+            if (next < i) return end();
+            return lower_bound(next);
+        }
+
+        // there are bits left, use least significant bit of those
+        if (it->first == i >> LEAF_INDEX_WIDTH) {
+            mask &= ((~uint64_t(0)) << (i & LEAF_INDEX_MASK));  // remove all bits before pos i
+        }
+
+        // compute value represented by least significant bit
+        index_type pos = __builtin_ctzll(mask);
+
+        // remove this bit as well
+        mask = mask & ~(1ull << pos);
+
+        // construct value of this located bit
+        index_type val = (it->first << LEAF_INDEX_WIDTH) | pos;
+        return iterator(it, mask, val);
+    }
+
+    /**
+     * Locates an iterator to the first element in this sparse bit map than is greater
+     * than the given index.
+     */
+    iterator upper_bound(index_type i) const {
+        if (i == std::numeric_limits<index_type>::max()) {
+            return end();
+        }
+        return lower_bound(i + 1);
+    }
+
+    /**
+     * A debugging utility printing the internal structure of this map to the
+     * given output stream.
+     */
+    void dump(bool detail = false, std::ostream& out = std::cout) const {
+        store.dump(detail, out);
+    }
+
+    /**
+     * Provides write-protected access to the internal store for running
+     * analysis on the data structure.
+     */
+    const data_store_t& getStore() const {
+        return store;
+    }
+};
+
+// ---------------------------------------------------------------------
+//                              TRIE
+// ---------------------------------------------------------------------
+
+namespace detail::brie {
+
+/**
+ * An iterator over the stored entries.
+ *
+ * Iterators for tries consist of a top-level iterator maintaining the
+ * master copy of a materialized tuple and a recursively nested iterator
+ * core -- one for each nested trie level.
+ */
+template <typename Value, typename IterCore>
+class TrieIterator {
+    template <unsigned Len, unsigned Pos, unsigned Dimensions>
+    friend struct fix_binding;
+
+    template <unsigned Dimensions>
+    friend struct fix_lower_bound;
+
+    template <unsigned Dimensions>
+    friend struct fix_upper_bound;
+
+    template <unsigned Pos, unsigned Dimensions>
+    friend struct fix_first;
+
+    template <unsigned Dimensions>
+    friend struct fix_first_nested;
+
+    template <typename A, typename B>
+    friend class TrieIterator;
+
+    // remove ref-qual (if any); this can happen if we're a iterator-view
+    using iter_core_arg_type = typename std::remove_reference_t<IterCore>::store_iter;
+
+    Value value;         // the value currently pointed to
+    IterCore iter_core;  // the wrapped iterator
+
+    // return an ephemeral nested iterator-view (view -> mutating us mutates our parent)
+    // NB: be careful that the lifetime of this iterator-view doesn't exceed that of its parent.
+    auto getNestedView() {
+        auto& nested_iter_ref = iter_core.getNested();  // by ref (this is critical, we're a view, not a copy)
+        auto nested_val = tail(value);
+        return TrieIterator<decltype(nested_val), decltype(nested_iter_ref)>(
+                std::move(nested_val), nested_iter_ref);
+    }
+
+    // special constructor for iterator-views (see `getNestedView`)
+    explicit TrieIterator(Value value, IterCore iter_core) : value(std::move(value)), iter_core(iter_core) {}
+
+public:
+    TrieIterator() = default;  // default constructor -- creating an end-iterator
+    TrieIterator(const TrieIterator&) = default;
+    TrieIterator(TrieIterator&&) = default;
+    TrieIterator& operator=(const TrieIterator&) = default;
+    TrieIterator& operator=(TrieIterator&&) = default;
+
+    explicit TrieIterator(iter_core_arg_type param) : iter_core(std::move(param), value) {}
+
+    // the equality operator as required by the iterator concept
+    bool operator==(const TrieIterator& other) const {
+        // equivalent if pointing to the same value
+        return iter_core == other.iter_core;
+    }
+
+    // the not-equality operator as required by the iterator concept
+    bool operator!=(const TrieIterator& other) const {
+        return !(*this == other);
+    }
+
+    const Value& operator*() const {
+        return value;
+    }
+
+    const Value* operator->() const {
+        return &value;
+    }
+
+    TrieIterator& operator++() {
+        iter_core.inc(value);
+        return *this;
+    }
+
+    TrieIterator operator++(int) {
+        auto cpy = *this;
+        ++(*this);
+        return cpy;
+    }
+
+    // enables this iterator to be printed (for debugging)
+    void print(std::ostream& out) const {
+        out << "iter(" << iter_core << " -> " << value << ")";
+    }
+
+    friend std::ostream& operator<<(std::ostream& out, const TrieIterator& iter) {
+        iter.print(out);
+        return out;
+    }
+};
+
+template <unsigned Dim>
+struct TrieTypes;
+
+/**
+ * A base class for the Trie implementation allowing various
+ * specializations of the Trie template to inherit common functionality.
+ *
+ * @tparam Dim the number of dimensions / arity of the stored tuples
+ * @tparam Derived the type derived from this base class
+ */
+template <unsigned Dim, typename Derived>
+class TrieBase {
+    Derived& impl() {
+        return static_cast<Derived&>(*this);
+    }
+
+    const Derived& impl() const {
+        return static_cast<const Derived&>(*this);
+    }
+
+protected:
+    using types = TrieTypes<Dim>;
+    using store_type = typename types::store_type;
+
+    store_type store;
+
+public:
+    using const_entry_span_type = typename types::const_entry_span_type;
+    using entry_span_type = typename types::entry_span_type;
+    using entry_type = typename types::entry_type;
+    using iterator = typename types::iterator;
+    using iterator_core = typename types::iterator_core;
+    using op_context = typename types::op_context;
+
+    /**
+     * Inserts all tuples stored within the given trie into this trie.
+     * This operation is considerably more efficient than the consecutive
+     * insertion of the elements in other into this trie.
+     *
+     * @param other the elements to be inserted into this trie
+     */
+    void insertAll(const TrieBase& other) {
+        store.addAll(other.store);
+    }
+
+    /**
+     * Provides protected access to the internally maintained store.
+     */
+    const store_type& getStore() const {
+        return store;
+    }
+
+    /**
+     * Determines whether this trie is empty or not.
+     */
+    bool empty() const {
+        return store.empty();
+    }
+
+    /**
+     * Obtains an iterator referencing the first element stored within this trie.
+     */
+    iterator begin() const {
+        return empty() ? end() : iterator(store.begin());
+    }
+
+    /**
+     * Obtains an iterator referencing the position after the last element stored
+     * within this trie.
+     */
+    iterator end() const {
+        return iterator();
+    }
+
+    iterator find(const_entry_span_type entry, op_context& ctxt) const {
+        auto range = impl().template getBoundaries<Dim>(entry, ctxt);
+        return range.empty() ? range.end() : range.begin();
+    }
+
+    // implemented by `Derived`:
+    //      bool insert(const entry_type& tuple, op_context& ctxt);
+    //      bool contains(const_entry_span_type tuple, op_context& ctxt) const;
+    //      bool lower_bound(const_entry_span_type tuple, op_context& ctxt) const;
+    //      bool upper_bound(const_entry_span_type tuple, op_context& ctxt) const;
+    //      template <unsigned levels>
+    //      range<iterator> getBoundaries(const_entry_span_type, op_context&) const;
+
+    // -- operation wrappers --
+
+    template <unsigned levels>
+    range<iterator> getBoundaries(const_entry_span_type entry) const {
+        op_context ctxt;
+        return impl().template getBoundaries<levels>(entry, ctxt);
+    }
+
+    template <unsigned levels>
+    range<iterator> getBoundaries(const entry_type& entry, op_context& ctxt) const {
+        return impl().template getBoundaries<levels>(const_entry_span_type(entry), ctxt);
+    }
+
+    template <unsigned levels>
+    range<iterator> getBoundaries(const entry_type& entry) const {
+        return impl().template getBoundaries<levels>(const_entry_span_type(entry));
+    }
+
+    template <unsigned levels, typename... Values, typename = std::enable_if_t<(isRamType<Values> && ...)>>
+    range<iterator> getBoundaries(Values... values) const {
+        return impl().template getBoundaries<levels>(entry_type{ramBitCast(values)...});
+    }
+
+// declare a initialiser-list compatible overload for a given function
+#define BRIE_OVERLOAD_INIT_LIST(fn, constness)                     \
+    auto fn(const_entry_span_type entry) constness {               \
+        op_context ctxt;                                           \
+        return impl().fn(entry, ctxt);                             \
+    }                                                              \
+    auto fn(const entry_type& entry, op_context& ctxt) constness { \
+        return impl().fn(const_entry_span_type(entry), ctxt);      \
+    }                                                              \
+    auto fn(const entry_type& entry) constness {                   \
+        return impl().fn(const_entry_span_type(entry));            \
+    }
+
+    BRIE_OVERLOAD_INIT_LIST(insert, )
+    BRIE_OVERLOAD_INIT_LIST(find, const)
+    BRIE_OVERLOAD_INIT_LIST(contains, const)
+    BRIE_OVERLOAD_INIT_LIST(lower_bound, const)
+    BRIE_OVERLOAD_INIT_LIST(upper_bound, const)
+
+#undef BRIE_OVERLOAD_INIT_LIST
+
+    /* -------------- operator hint statistics ----------------- */
+
+    // an aggregation of statistical values of the hint utilization
+    struct hint_statistics {
+        // the counter for insertion operations
+        CacheAccessCounter inserts;
+
+        // the counter for contains operations
+        CacheAccessCounter contains;
+
+        // the counter for get_boundaries operations
+        CacheAccessCounter get_boundaries;
+    };
+
+protected:
+    // the hint statistic of this b-tree instance
+    mutable hint_statistics hint_stats;
+
+public:
+    void printStats(std::ostream& out) const {
+        out << "---------------------------------\n";
+        out << "  insert-hint (hits/misses/total): " << hint_stats.inserts.getHits() << "/"
+            << hint_stats.inserts.getMisses() << "/" << hint_stats.inserts.getAccesses() << "\n";
+        out << "  contains-hint (hits/misses/total):" << hint_stats.contains.getHits() << "/"
+            << hint_stats.contains.getMisses() << "/" << hint_stats.contains.getAccesses() << "\n";
+        out << "  get-boundaries-hint (hits/misses/total):" << hint_stats.get_boundaries.getHits() << "/"
+            << hint_stats.get_boundaries.getMisses() << "/" << hint_stats.get_boundaries.getAccesses()
+            << "\n";
+        out << "---------------------------------\n";
+    }
+};
+
+template <unsigned Dim>
+struct TrieTypes;
+
+// FIXME: THIS KILLS COMPILE PERF - O(n^2)
+/**
+ * A functor extracting a reference to a nested iterator core from an enclosing
+ * iterator core.
+ */
+template <unsigned Level>
+struct get_nested_iter_core {
+    template <typename IterCore>
+    auto operator()(IterCore& core) -> decltype(get_nested_iter_core<Level - 1>()(core.getNested())) {
+        return get_nested_iter_core<Level - 1>()(core.getNested());
+    }
+};
+
+template <>
+struct get_nested_iter_core<0> {
+    template <typename IterCore>
+    IterCore& operator()(IterCore& core) {
+        return core;
+    }
+};
+
+// FIXME: THIS KILLS COMPILE PERF - O(n^2)
+/**
+ * A functor initializing an iterator upon creation to reference the first
+ * element in the associated Trie.
+ */
+template <unsigned Pos, unsigned Dim>
+struct fix_first {
+    template <unsigned bits, typename iterator>
+    void operator()(const SparseBitMap<bits>& store, iterator& iter) const {
+        // set iterator to first in store
+        auto first = store.begin();
+        get_nested_iter_core<Pos>()(iter.iter_core).setIterator(first);
+        iter.value[Pos] = *first;
+    }
+
+    template <typename Store, typename iterator>
+    void operator()(const Store& store, iterator& iter) const {
+        // set iterator to first in store
+        auto first = store.begin();
+        get_nested_iter_core<Pos>()(iter.iter_core).setIterator(first);
+        iter.value[Pos] = first->first;
+        // and continue recursively
+        fix_first<Pos + 1, Dim>()(first->second->getStore(), iter);
+    }
+};
+
+template <unsigned Dim>
+struct fix_first<Dim, Dim> {
+    template <typename Store, typename iterator>
+    void operator()(const Store&, iterator&) const {
+        // terminal case => nothing to do
+    }
+};
+
+template <unsigned Dim>
+struct fix_first_nested {
+    template <unsigned bits, typename iterator>
+    void operator()(const SparseBitMap<bits>& store, iterator&& iter) const {
+        // set iterator to first in store
+        auto first = store.begin();
+        iter.value[0] = *first;
+        iter.iter_core.setIterator(std::move(first));
+    }
+
+    template <typename Store, typename iterator>
+    void operator()(const Store& store, iterator&& iter) const {
+        // set iterator to first in store
+        auto first = store.begin();
+        iter.value[0] = first->first;
+        iter.iter_core.setIterator(std::move(first));
+        // and continue recursively
+        fix_first_nested<Dim - 1>()(first->second->getStore(), iter.getNestedView());
+    }
+};
+
+// TODO: rewrite to erase `Pos` and `Len` arguments. this can cause a template instance explosion
+/**
+ * A functor initializing an iterator upon creation to reference the first element
+ * exhibiting a given prefix within a given Trie.
+ */
+template <unsigned Len, unsigned Pos, unsigned Dim>
+struct fix_binding {
+    template <unsigned bits, typename iterator, typename entry_type>
+    bool operator()(
+            const SparseBitMap<bits>& store, iterator& begin, iterator& end, const entry_type& entry) const {
+        // search in current level
+        auto cur = store.find(entry[Pos]);
+
+        // if not present => fail
+        if (cur == store.end()) return false;
+
+        // take current value
+        get_nested_iter_core<Pos>()(begin.iter_core).setIterator(cur);
+        ++cur;
+        get_nested_iter_core<Pos>()(end.iter_core).setIterator(cur);
+
+        // update iterator value
+        begin.value[Pos] = entry[Pos];
+
+        // no more remaining levels to fix
+        return true;
+    }
+
+    template <typename Store, typename iterator, typename entry_type>
+    bool operator()(const Store& store, iterator& begin, iterator& end, const entry_type& entry) const {
+        // search in current level
+        auto cur = store.find(entry[Pos]);
+
+        // if not present => fail
+        if (cur == store.end()) return false;
+
+        // take current value as start
+        get_nested_iter_core<Pos>()(begin.iter_core).setIterator(cur);
+
+        // update iterator value
+        begin.value[Pos] = entry[Pos];
+
+        // fix remaining nested iterators
+        auto res = fix_binding<Len - 1, Pos + 1, Dim>()(cur->second->getStore(), begin, end, entry);
+
+        // update end of iterator
+        if (get_nested_iter_core<Pos + 1>()(end.iter_core).getIterator() == cur->second->getStore().end()) {
+            ++cur;
+            if (cur != store.end()) {
+                fix_first<Pos + 1, Dim>()(cur->second->getStore(), end);
+            }
+        }
+        get_nested_iter_core<Pos>()(end.iter_core).setIterator(cur);
+
+        // done
+        return res;
+    }
+};
+
+template <unsigned Pos, unsigned Dim>
+struct fix_binding<0, Pos, Dim> {
+    template <unsigned bits, typename iterator, typename entry_type>
+    bool operator()(const SparseBitMap<bits>& store, iterator& begin, iterator& /* end */,
+            const entry_type& /* entry */) const {
+        // move begin to begin of store
+        auto a = store.begin();
+        get_nested_iter_core<Pos>()(begin.iter_core).setIterator(a);
+        begin.value[Pos] = *a;
+
+        return true;
+    }
+
+    template <typename Store, typename iterator, typename entry_type>
+    bool operator()(const Store& store, iterator& begin, iterator& end, const entry_type& entry) const {
+        // move begin to begin of store
+        auto a = store.begin();
+        get_nested_iter_core<Pos>()(begin.iter_core).setIterator(a);
+        begin.value[Pos] = a->first;
+
+        // continue recursively
+        fix_binding<0, Pos + 1, Dim>()(a->second->getStore(), begin, end, entry);
+        return true;
+    }
+};
+
+template <unsigned Dim>
+struct fix_binding<0, Dim, Dim> {
+    template <typename Store, typename iterator, typename entry_type>
+    bool operator()(const Store& /* store */, iterator& /* begin */, iterator& /* end */,
+            const entry_type& /* entry */) const {
+        // nothing more to do
+        return true;
+    }
+};
+
+/**
+ * A functor initializing an iterator upon creation to reference the first element
+ * within a given Trie being not less than a given value .
+ */
+template <unsigned Dim>
+struct fix_lower_bound {
+    using types = TrieTypes<Dim>;
+    using const_entry_span_type = typename types::const_entry_span_type;
+
+    template <unsigned bits, typename iterator>
+    bool operator()(const SparseBitMap<bits>& store, iterator&& iter, const_entry_span_type entry) const {
+        auto cur = store.lower_bound(entry[0]);
+        if (cur == store.end()) return false;
+        assert(entry[0] <= brie_element_type(*cur));
+
+        iter.iter_core.setIterator(cur);
+        iter.value[0] = *cur;
+        return true;
+    }
+
+    template <typename Store, typename iterator>
+    bool operator()(const Store& store, iterator&& iter, const_entry_span_type entry) const {
+        auto cur = store.lowerBound(entry[0]);  // search in current level
+        if (cur == store.end()) return false;   // if no lower boundary is found, be done
+        assert(brie_element_type(cur->first) >= entry[0]);
+
+        // if the lower bound is higher than the requested value, go to first in subtree
+        if (brie_element_type(cur->first) > entry[0]) {
+            iter.iter_core.setIterator(cur);
+            iter.value[0] = cur->first;
+            fix_first_nested<Dim - 1>()(cur->second->getStore(), iter.getNestedView());
+            return true;
+        }
+
+        // attempt to fix the rest
+        if (!fix_lower_bound<Dim - 1>()(cur->second->getStore(), iter.getNestedView(), tail(entry))) {
+            // if it does not work, since there are no matching elements in this branch, go to next
+            auto sub = copy(entry);
+            sub[0] += 1;
+            for (std::size_t i = 1; i < Dim; ++i)
+                sub[i] = 0;
+
+            return (*this)(store, iter, sub);
+        }
+
+        iter.iter_core.setIterator(cur);  // remember result
+        iter.value[0] = cur->first;       // update iterator value
+        return true;
+    }
+};
+
+/**
+ * A functor initializing an iterator upon creation to reference the first element
+ * within a given Trie being greater than a given value .
+ */
+template <unsigned Dim>
+struct fix_upper_bound {
+    using types = TrieTypes<Dim>;
+    using const_entry_span_type = typename types::const_entry_span_type;
+
+    template <unsigned bits, typename iterator>
+    bool operator()(const SparseBitMap<bits>& store, iterator&& iter, const_entry_span_type entry) const {
+        auto cur = store.upper_bound(entry[0]);
+        if (cur == store.end()) return false;
+        assert(entry[0] <= brie_element_type(*cur));
+
+        iter.iter_core.setIterator(cur);
+        iter.value[0] = *cur;
+        return true;  // no more remaining levels to fix
+    }
+
+    template <typename Store, typename iterator>
+    bool operator()(const Store& store, iterator&& iter, const_entry_span_type entry) const {
+        auto cur = store.lowerBound(entry[0]);  // search in current level
+        if (cur == store.end()) return false;   // if no upper boundary is found, be done
+        assert(brie_element_type(cur->first) >= entry[0]);
+
+        // if the lower bound is higher than the requested value, go to first in subtree
+        if (brie_element_type(cur->first) > entry[0]) {
+            iter.iter_core.setIterator(cur);
+            iter.value[0] = cur->first;
+            fix_first_nested<Dim - 1>()(cur->second->getStore(), iter.getNestedView());
+            return true;
+        }
+
+        // attempt to fix the rest
+        if (!fix_upper_bound<Dim - 1>()(cur->second->getStore(), iter.getNestedView(), tail(entry))) {
+            // if it does not work, since there are no matching elements in this branch, go to next
+            auto sub = copy(entry);
+            sub[0] += 1;
+            for (std::size_t i = 1; i < Dim; ++i)
+                sub[i] = 0;
+
+            return (*this)(store, iter, sub);
+        }
+
+        iter.iter_core.setIterator(cur);  // remember result
+        iter.value[0] = cur->first;       // update iterator value
+        return true;
+    }
+};
+
+template <unsigned Dim>
+struct TrieTypes {
+    using entry_type = std::array<brie_element_type, Dim>;
+    using entry_span_type = span<brie_element_type, Dim>;
+    using const_entry_span_type = span<const brie_element_type, Dim>;
+
+    // the type of the nested tries (1 dimension less)
+    using nested_trie_type = Trie<Dim - 1>;
+
+    // the merge operation capable of merging two nested tries
+    struct nested_trie_merger {
+        nested_trie_type* operator()(nested_trie_type* a, const nested_trie_type* b) const {
+            if (!b) return a;
+            if (!a) return new nested_trie_type(*b);
+            a->insertAll(*b);
+            return a;
+        }
+    };
+
+    // the operation capable of cloning a nested trie
+    struct nested_trie_cloner {
+        nested_trie_type* operator()(nested_trie_type* a) const {
+            if (!a) return a;
+            return new nested_trie_type(*a);
+        }
+    };
+
+    // the data structure utilized for indexing nested tries
+    using store_type = SparseArray<nested_trie_type*,
+            6,  // = 2^6 entries per block
+            nested_trie_merger, nested_trie_cloner>;
+
+    // The iterator core for trie iterators involving this level.
+    struct iterator_core {
+        using store_iter = typename store_type::iterator;  // the iterator for the current level
+        using nested_core_iter = typename nested_trie_type::iterator_core;  // the type of the nested iterator
+
+    private:
+        store_iter iter;
+        nested_core_iter nested;
+
+    public:
+        iterator_core() = default;  // default -> end iterator
+
+        iterator_core(store_iter store_iter, entry_span_type entry) : iter(std::move(store_iter)) {
+            entry[0] = iter->first;
+            nested = {iter->second->getStore().begin(), tail(entry)};
+        }
+
+        void setIterator(store_iter store_iter) {
+            iter = std::move(store_iter);
+        }
+
+        store_iter& getIterator() {
+            return iter;
+        }
+
+        nested_core_iter& getNested() {
+            return nested;
+        }
+
+        bool inc(entry_span_type entry) {
+            // increment nested iterator
+            auto nested_entry = tail(entry);
+            if (nested.inc(nested_entry)) return true;
+
+            // increment the iterator on this level
+            ++iter;
+
+            // check whether the end has been reached
+            if (iter.isEnd()) return false;
+
+            // otherwise update entry value
+            entry[0] = iter->first;
+
+            // and restart nested
+            nested = {iter->second->getStore().begin(), nested_entry};
+            return true;
+        }
+
+        bool operator==(const iterator_core& other) const {
+            return nested == other.nested && iter == other.iter;
+        }
+
+        bool operator!=(const iterator_core& other) const {
+            return !(*this == other);
+        }
+
+        // enables this iterator core to be printed (for debugging)
+        void print(std::ostream& out) const {
+            out << iter << " | " << nested;
+        }
+
+        friend std::ostream& operator<<(std::ostream& out, const iterator_core& iter) {
+            iter.print(out);
+            return out;
+        }
+    };
+
+    using iterator = TrieIterator<entry_type, iterator_core>;
+
+    // the operation context aggregating all operation contexts of nested structures
+    struct op_context {
+        using local_ctxt = typename store_type::op_context;
+        using nested_ctxt = typename nested_trie_type::op_context;
+
+        // for insert and contain
+        local_ctxt local{};
+        brie_element_type lastQuery{};
+        nested_trie_type* lastNested{nullptr};
+        nested_ctxt nestedCtxt{};
+
+        // for boundaries
+        unsigned lastBoundaryLevels{Dim + 1};
+        entry_type lastBoundaryRequest{};
+        range<iterator> lastBoundaries{iterator(), iterator()};
+    };
+};
+
+template <>
+struct TrieTypes<1u> {
+    using entry_type = std::array<brie_element_type, 1>;
+    using entry_span_type = span<brie_element_type, 1>;
+    using const_entry_span_type = span<const brie_element_type, 1>;
+
+    // the map type utilized internally
+    using store_type = SparseBitMap<>;
+    using op_context = store_type::op_context;
+
+    /**
+     * The iterator core of this level contributing to the construction of
+     * a composed trie iterator.
+     */
+    struct iterator_core {
+        using store_iter = typename store_type::iterator;
+
+    private:
+        store_iter iter;
+
+    public:
+        iterator_core() = default;  // default end-iterator constructor
+
+        iterator_core(store_iter store_iter, entry_span_type entry)
+                : iter(std::move(store_iter))  // NOLINT : mistaken warning -`store_iter` is not const-qual
+        {
+            entry[0] = brie_element_type(*iter);
+        }
+
+        void setIterator(store_iter store_iter) {
+            iter = std::move(store_iter);  // NOLINT : mistaken warning - `store_iter` is not const-qual
+        }
+
+        store_iter& getIterator() {
+            return iter;
+        }
+
+        bool inc(entry_span_type entry) {
+            // increment the iterator on this level
+            ++iter;
+
+            // check whether the end has been reached
+            if (iter.isEnd()) return false;
+
+            // otherwise update entry value
+            entry[0] = brie_element_type(*iter);
+            return true;
+        }
+
+        bool operator==(const iterator_core& other) const {
+            return iter == other.iter;
+        }
+
+        bool operator!=(const iterator_core& other) const {
+            return !(*this == other);
+        }
+
+        // enables this iterator core to be printed (for debugging)
+        void print(std::ostream& out) const {
+            out << iter;
+        }
+
+        friend std::ostream& operator<<(std::ostream& out, const iterator_core& iter) {
+            iter.print(out);
+            return out;
+        }
+    };
+
+    using iterator = TrieIterator<entry_type, iterator_core>;
+};
+
+}  // namespace detail::brie
+
+// use an inner class so `TrieN` is fully defined before the recursion, allowing us to use
+// `op_context` in `TrieBase`
+template <unsigned Dim>
+class Trie : public TrieBase<Dim, Trie<Dim>> {
+    template <unsigned N>
+    friend class Trie;
+
+    // a shortcut for the common base class type
+    using base = TrieBase<Dim, Trie<Dim>>;
+    using types = TrieTypes<Dim>;
+    using nested_trie_type = typename types::nested_trie_type;
+    using store_type = typename types::store_type;
+
+    using base::store;
+
+public:
+    using const_entry_span_type = typename types::const_entry_span_type;
+    using entry_span_type = typename types::entry_span_type;
+    using entry_type = typename types::entry_type;
+    using iterator = typename types::iterator;
+    using iterator_core = typename types::iterator_core;
+    using op_context = typename types::op_context;
+    // type aliases for compatibility with `BTree` and others
+    using operation_hints = op_context;
+    using element_type = entry_type;
+
+    using base::begin;
+    using base::contains;
+    using base::empty;
+    using base::end;
+    using base::find;
+    using base::getBoundaries;
+    using base::insert;
+    using base::lower_bound;
+    using base::upper_bound;
+
+    ~Trie() {
+        clear();
+    }
+
+    /**
+     * Determines the number of entries in this trie.
+     */
+    std::size_t size() const {
+        // the number of elements is lazy-evaluated
+        std::size_t res = 0;
+        for (auto&& [_, v] : store)
+            res += v->size();
+
+        return res;
+    }
+
+    /**
+     * Computes the total memory usage of this data structure.
+     */
+    std::size_t getMemoryUsage() const {
+        // compute the total memory usage of this level
+        auto res = sizeof(*this) - sizeof(store) + store.getMemoryUsage();
+        for (auto&& [_, v] : store)
+            res += v->getMemoryUsage();  // add the memory usage of sub-levels
+
+        return res;
+    }
+
+    /**
+     * Removes all entries within this trie.
+     */
+    void clear() {
+        // delete lower levels manually
+        // (can't use `Own` b/c we need `atomic` instances and those require trivial assignment)
+        for (auto& cur : store)
+            delete cur.second;
+
+        // clear store
+        store.clear();
+    }
+
+    /**
+     * Inserts a new entry. A operation context may be provided to exploit temporal
+     * locality.
+     *
+     * @param tuple the entry to be added
+     * @param ctxt the operation context to be utilized
+     * @return true if the same tuple hasn't been present before, false otherwise
+     */
+    bool insert(const_entry_span_type tuple, op_context& ctxt) {
+        using value_t = typename store_type::value_type;
+        using atomic_value_t = typename store_type::atomic_value_type;
+
+        // check context
+        if (ctxt.lastNested && ctxt.lastQuery == tuple[0]) {
+            base::hint_stats.inserts.addHit();
+            return ctxt.lastNested->insert(tail(tuple), ctxt.nestedCtxt);
+        }
+
+        base::hint_stats.inserts.addMiss();
+
+        // lookup nested
+        atomic_value_t& next = store.getAtomic(tuple[0], ctxt.local);
+
+        // get pure pointer to next level
+        value_t nextPtr = next;
+
+        // conduct a lock-free lazy-creation of nested trees
+        if (!nextPtr) {
+            // create a sub-tree && register it atomically
+            auto newNested = mk<nested_trie_type>();
+            if (next.compare_exchange_weak(nextPtr, newNested.get())) {
+                nextPtr = newNested.release();  // worked, ownership is acquired by `store`
+            }
+            // otherwise some other thread was faster => use its version
+        }
+
+        // make sure a next has been established
+        assert(nextPtr);
+
+        // clear context if necessary
+        if (nextPtr != ctxt.lastNested) {
+            ctxt.lastQuery = tuple[0];
+            ctxt.lastNested = nextPtr;
+            ctxt.nestedCtxt = {};
+        }
+
+        // conduct recursive step
+        return nextPtr->insert(tail(tuple), ctxt.nestedCtxt);
+    }
+
+    bool contains(const_entry_span_type tuple, op_context& ctxt) const {
+        // check context
+        if (ctxt.lastNested && ctxt.lastQuery == tuple[0]) {
+            base::hint_stats.contains.addHit();
+            return ctxt.lastNested->contains(tail(tuple), ctxt.nestedCtxt);
+        }
+
+        base::hint_stats.contains.addMiss();
+
+        // lookup next step
+        auto next = store.lookup(tuple[0], ctxt.local);
+
+        // clear context if necessary
+        if (next != ctxt.lastNested) {
+            ctxt.lastQuery = tuple[0];
+            ctxt.lastNested = next;
+            ctxt.nestedCtxt = {};
+        }
+
+        // conduct recursive step
+        return next && next->contains(tail(tuple), ctxt.nestedCtxt);
+    }
+
+    /**
+     * Obtains a range of elements matching the prefix of the given entry up to
+     * levels elements. A operation context may be provided to exploit temporal
+     * locality.
+     *
+     * @tparam levels the length of the requested matching prefix
+     * @param entry the entry to be looking for
+     * @param ctxt the operation context to be utilized
+     * @return the corresponding range of matching elements
+     */
+    template <unsigned levels>
+    range<iterator> getBoundaries(const_entry_span_type entry, op_context& ctxt) const {
+        // if nothing is bound => just use begin and end
+        if constexpr (levels == 0) return make_range(begin(), end());
+
+        // check context
+        if (ctxt.lastBoundaryLevels == levels) {
+            bool fit = true;
+            for (unsigned i = 0; i < levels; ++i) {
+                fit = fit && (entry[i] == ctxt.lastBoundaryRequest[i]);
+            }
+
+            // if it fits => take it
+            if (fit) {
+                base::hint_stats.get_boundaries.addHit();
+                return ctxt.lastBoundaries;
+            }
+        }
+
+        // the hint has not been a hit
+        base::hint_stats.get_boundaries.addMiss();
+
+        // start with two end iterators
+        iterator begin{};
+        iterator end{};
+
+        // adapt them level by level
+        auto found = fix_binding<levels, 0, Dim>()(store, begin, end, entry);
+        if (!found) return make_range(iterator(), iterator());
+
+        // update context
+        static_assert(std::tuple_size_v<decltype(ctxt.lastBoundaryRequest)> == Dim);
+        static_assert(std::tuple_size_v<decltype(entry)> == Dim);
+        ctxt.lastBoundaryLevels = levels;
+        std::copy_n(entry.begin(), Dim, ctxt.lastBoundaryRequest.begin());
+        ctxt.lastBoundaries = make_range(begin, end);
+
+        // use the result
+        return ctxt.lastBoundaries;
+    }
+
+    /**
+     * Obtains an iterator to the first element not less than the given entry value.
+     *
+     * @param entry the lower bound for this search
+     * @param ctxt the operation context to be utilized
+     * @return an iterator addressing the first element in this structure not less than the given value
+     */
+    iterator lower_bound(const_entry_span_type entry, op_context& /* ctxt */) const {
+        // start with a default-initialized iterator
+        iterator res;
+
+        // adapt it level by level
+        bool found = fix_lower_bound<Dim>()(store, res, entry);
+
+        // use the result
+        return found ? res : end();
+    }
+
+    /**
+     * Obtains an iterator to the first element greater than the given entry value, or end if there is no
+     * such element.
+     *
+     * @param entry the upper bound for this search
+     * @param ctxt the operation context to be utilized
+     * @return an iterator addressing the first element in this structure greater than the given value
+     */
+    iterator upper_bound(const_entry_span_type entry, op_context& /* ctxt */) const {
+        // start with a default-initialized iterator
+        iterator res;
+
+        // adapt it level by level
+        bool found = fix_upper_bound<Dim>()(store, res, entry);
+
+        // use the result
+        return found ? res : end();
+    }
+
+    /**
+     * Computes a partition of an approximate number of chunks of the content
+     * of this trie. Thus, the union of the resulting set of disjoint ranges is
+     * equivalent to the content of this trie.
+     *
+     * @param chunks the number of chunks requested
+     * @return a list of sub-ranges forming a partition of the content of this trie
+     */
+    std::vector<range<iterator>> partition(unsigned chunks = 500) const {
+        std::vector<range<iterator>> res;
+
+        // shortcut for empty trie
+        if (this->empty()) return res;
+
+        // use top-level elements for partitioning
+        int step = std::max(store.size() / chunks, std::size_t(1));
+
+        int c = 1;
+        auto priv = begin();
+        for (auto it = store.begin(); it != store.end(); ++it, c++) {
+            if (c % step != 0 || c == 1) {
+                continue;
+            }
+            auto cur = iterator(it);
+            res.push_back(make_range(priv, cur));
+            priv = cur;
+        }
+        // add final chunk
+        res.push_back(make_range(priv, end()));
+        return res;
+    }
+};
+
+/**
+ * A template specialization for tries representing a set.
+ * For improved memory efficiency, this level is the leaf-node level
+ * of all tries exhibiting an arity >= 1. Internally, values are stored utilizing
+ * sparse bit maps.
+ */
+template <>
+class Trie<1u> : public TrieBase<1u, Trie<1u>> {
+    using base = TrieBase<1u, Trie<1u>>;
+    using types = TrieTypes<1u>;
+    using store_type = typename types::store_type;
+
+    using base::store;
+
+public:
+    using const_entry_span_type = typename types::const_entry_span_type;
+    using entry_span_type = typename types::entry_span_type;
+    using entry_type = typename types::entry_type;
+    using iterator = typename types::iterator;
+    using iterator_core = typename types::iterator_core;
+    using op_context = typename types::op_context;
+    // type aliases for compatibility with `BTree` and others
+    using operation_hints = op_context;
+    using element_type = entry_type;
+
+    using base::begin;
+    using base::contains;
+    using base::empty;
+    using base::end;
+    using base::find;
+    using base::getBoundaries;
+    using base::insert;
+    using base::lower_bound;
+    using base::upper_bound;
+
+    /**
+     * Determines the number of entries in this trie.
+     */
+    std::size_t size() const {
+        return store.size();
+    }
+
+    /**
+     * Computes the total memory usage of this data structure.
+     */
+    std::size_t getMemoryUsage() const {
+        // compute the total memory usage
+        return sizeof(*this) - sizeof(store) + store.getMemoryUsage();
+    }
+
+    /**
+     * Removes all elements form this trie.
+     */
+    void clear() {
+        store.clear();
+    }
+
+    /**
+     * Inserts the given tuple into this trie.
+     * An operation context can be provided to exploit temporal locality.
+     *
+     * @param tuple the tuple to be inserted
+     * @param ctxt an operation context for exploiting temporal locality
+     * @return true if the tuple has not been present before, false otherwise
+     */
+    bool insert(const_entry_span_type tuple, op_context& ctxt) {
+        return store.set(tuple[0], ctxt);
+    }
+
+    /**
+     * Determines whether the given tuple is present in this trie or not.
+     * An operation context can be provided to exploit temporal locality.
+     *
+     * @param tuple the tuple to be tested
+     * @param ctxt an operation context for exploiting temporal locality
+     * @return true if present, false otherwise
+     */
+    bool contains(const_entry_span_type tuple, op_context& ctxt) const {
+        return store.test(tuple[0], ctxt);
+    }
+
+    // ---------------------------------------------------------------------
+    //                           Iterator
+    // ---------------------------------------------------------------------
+
+    /**
+     * Obtains a partition of this tire such that the resulting list of ranges
+     * cover disjoint subsets of the elements stored in this trie. Their union
+     * is equivalent to the content of this trie.
+     */
+    std::vector<range<iterator>> partition(unsigned chunks = 500) const {
+        std::vector<range<iterator>> res;
+
+        // shortcut for empty trie
+        if (this->empty()) return res;
+
+        // use top-level elements for partitioning
+        int step = static_cast<int>(std::max(store.size() / chunks, std::size_t(1)));
+
+        int c = 1;
+        auto priv = begin();
+        for (auto it = store.begin(); it != store.end(); ++it, c++) {
+            if (c % step != 0 || c == 1) {
+                continue;
+            }
+            auto cur = iterator(it);
+            res.push_back(make_range(priv, cur));
+            priv = cur;
+        }
+        // add final chunk
+        res.push_back(make_range(priv, end()));
+        return res;
+    }
+
+    /**
+     * Obtains a range of elements matching the prefix of the given entry up to
+     * levels elements. A operation context may be provided to exploit temporal
+     * locality.
+     *
+     * @tparam levels the length of the requested matching prefix
+     * @param entry the entry to be looking for
+     * @param ctxt the operation context to be utilized
+     * @return the corresponding range of matching elements
+     */
+    template <unsigned levels>
+    range<iterator> getBoundaries(const_entry_span_type entry, op_context& ctxt) const {
+        // for levels = 0
+        if (levels == 0) return make_range(begin(), end());
+        // for levels = 1
+        auto pos = store.find(entry[0], ctxt);
+        if (pos == store.end()) return make_range(end(), end());
+        auto next = pos;
+        ++next;
+        return make_range(iterator(pos), iterator(next));
+    }
+
+    iterator lower_bound(const_entry_span_type entry, op_context&) const {
+        return iterator(store.lower_bound(entry[0]));
+    }
+
+    iterator upper_bound(const_entry_span_type entry, op_context&) const {
+        return iterator(store.upper_bound(entry[0]));
+    }
+};
+
+}  // end namespace souffle
+
+namespace std {
+
+using namespace ::souffle::detail::brie;
+
+template <typename A>
+struct iterator_traits<SparseArrayIter<A>>
+        : forward_non_output_iterator_traits<typename SparseArrayIter<A>::value_type> {};
+
+template <typename A>
+struct iterator_traits<SparseBitMapIter<A>>
+        : forward_non_output_iterator_traits<typename SparseBitMapIter<A>::value_type> {};
+
+template <typename A, typename IterCore>
+struct iterator_traits<TrieIterator<A, IterCore>> : forward_non_output_iterator_traits<A> {};
+
+}  // namespace std
+
+#ifdef _WIN32
+#undef __sync_synchronize
+#undef __sync_bool_compare_and_swap
+#endif
diff --git a/cbits/souffle/datastructure/ConcurrentFlyweight.h b/cbits/souffle/datastructure/ConcurrentFlyweight.h
new file mode 100644
--- /dev/null
+++ b/cbits/souffle/datastructure/ConcurrentFlyweight.h
@@ -0,0 +1,477 @@
+/*
+ * Souffle - A Datalog Compiler
+ * Copyright (c) 2021, The Souffle Developers. All rights reserved
+ * Licensed under the Universal Permissive License v 1.0 as shown at:
+ * - https://opensource.org/licenses/UPL
+ * - <souffle root>/licenses/SOUFFLE-UPL.txt
+ */
+#pragma once
+
+#include "ConcurrentInsertOnlyHashMap.h"
+#include "souffle/utility/ParallelUtil.h"
+#include <cassert>
+#include <cstring>
+
+namespace souffle {
+
+/**
+ * A concurrent, almost lock-free associative datastructure that implements the
+ * Flyweight pattern.  Assigns a unique index to each inserted key. Elements
+ * cannot be removed, the datastructure can only grow.
+ *
+ * The datastructure enables a configurable number of concurrent access lanes.
+ * Access to the datastructure is lock-free between different lanes.
+ * Concurrent accesses through the same lane is sequential.
+ *
+ * Growing the datastructure requires to temporarily lock all lanes to let a
+ * single lane perform the growing operation. The global lock is amortized
+ * thanks to an exponential growth strategy.
+ *
+ */
+template <class LanesPolicy, class Key, class Hash = std::hash<Key>, class KeyEqual = std::equal_to<Key>,
+        class KeyFactory = details::Factory<Key>>
+class ConcurrentFlyweight {
+public:
+    using lane_id = typename LanesPolicy::lane_id;
+    using index_type = std::size_t;
+    using key_type = Key;
+    using value_type = std::pair<const Key, const index_type>;
+    using pointer = const value_type*;
+    using reference = const value_type&;
+
+    /// Iterator with concurrent access to the datastructure.
+    struct Iterator {
+        using iterator_category = std::input_iterator_tag;
+        using value_type = ConcurrentFlyweight::value_type;
+        using pointer = ConcurrentFlyweight::pointer;
+        using reference = ConcurrentFlyweight::reference;
+
+    private:
+        using slot_type = int64_t;
+
+        const ConcurrentFlyweight* This;
+
+        /// Access lane to the datastructure.
+        lane_id Lane;
+
+        /// Current slot.
+        slot_type Slot;
+
+        /// Next slot that might be unassigned.
+        slot_type NextMaybeUnassignedSlot;
+
+        /// Handle that owns the next slot that might be unassigned.
+        int64_t NextMaybeUnassignedHandle;
+
+        static constexpr int64_t End = std::numeric_limits<slot_type>::max();
+        static constexpr int64_t None = -1;
+
+        /// Converts from index to slot.
+        static slot_type slot(const index_type I) {
+            assert(I >= 0 && I <= std::numeric_limits<slot_type>::max());
+            return static_cast<int64_t>(I);
+        }
+
+        /// Converts from slot to index.
+        static index_type index(const slot_type S) {
+            assert(S >= 0 && S <= std::numeric_limits<index_type>::max());
+            return static_cast<index_type>(S);
+        }
+
+    public:
+        // The 'begin' iterator
+        Iterator(const ConcurrentFlyweight* This, const lane_id H)
+                : This(This), Lane(H), Slot(None), NextMaybeUnassignedSlot(0),
+                  NextMaybeUnassignedHandle(None) {
+            FindNextMaybeUnassignedSlot();
+            MoveToNextAssignedSlot();
+        }
+
+        // The 'end' iterator
+        Iterator(const ConcurrentFlyweight* This)
+                : This(This), Lane(0), Slot(End), NextMaybeUnassignedSlot(End),
+                  NextMaybeUnassignedHandle(None) {}
+
+        // The iterator starting at slot I, using access lane H.
+        Iterator(const ConcurrentFlyweight* This, const lane_id H, const index_type I)
+                : This(This), Lane(H), Slot(slot(I)), NextMaybeUnassignedSlot(slot(I)),
+                  NextMaybeUnassignedHandle(None) {
+            FindNextMaybeUnassignedSlot();
+            MoveToNextAssignedSlot();
+        }
+
+        Iterator(const Iterator& That)
+                : This(That.This), Lane(That.Lane), Slot(That.Slot),
+                  NextMaybeUnassignedSlot(That.NextMaybeUnassignedSlot),
+                  NextMaybeUnassignedHandle(That.NextMaybeUnassignedHandle) {}
+
+        Iterator(Iterator&& That)
+                : This(That.This), Lane(That.Lane), Slot(That.Slot),
+                  NextMaybeUnassignedSlot(That.NextMaybeUnassignedSlot),
+                  NextMaybeUnassignedHandle(That.NextMaybeUnassignedHandle) {}
+
+        Iterator& operator=(const Iterator& That) {
+            This = That.This;
+            Lane = That.Lane;
+            Slot = That.Slot;
+            NextMaybeUnassignedSlot = That.NextMaybeUnassignedSlot;
+            NextMaybeUnassignedHandle = That.NextMaybeUnassignedHandle;
+        }
+
+        Iterator& operator=(Iterator&& That) {
+            This = That.This;
+            Lane = That.Lane;
+            Slot = That.Slot;
+            NextMaybeUnassignedSlot = That.NextMaybeUnassignedSlot;
+            NextMaybeUnassignedHandle = That.NextMaybeUnassignedHandle;
+        }
+
+        reference operator*() const {
+            const auto Guard = This->Lanes.guard(Lane);
+            return *This->Slots[index(Slot)];
+        }
+
+        pointer operator->() const {
+            const auto Guard = This->Lanes.guard(Lane);
+            return This->Slots[index(Slot)];
+        }
+
+        Iterator& operator++() {
+            MoveToNextAssignedSlot();
+            return *this;
+        }
+
+        Iterator operator++(int) {
+            Iterator Tmp = *this;
+            ++(*this);
+            return Tmp;
+        }
+
+        bool operator==(const Iterator& That) const {
+            return (&This == &That.This) && (Slot == That.Slot);
+        }
+
+        bool operator!=(const Iterator& That) const {
+            return (This != That.This) || (Slot != That.Slot);
+        }
+
+    private:
+        /** Find next slot after Slot that is maybe unassigned. */
+        void FindNextMaybeUnassignedSlot() {
+            NextMaybeUnassignedSlot = End;
+            for (lane_id I = 0; I < This->Lanes.lanes(); ++I) {
+                const auto Lane = This->Lanes.guard(I);
+                if (This->Handles[I].NextSlot > Slot && This->Handles[I].NextSlot < NextMaybeUnassignedSlot) {
+                    NextMaybeUnassignedSlot = This->Handles[I].NextSlot;
+                    NextMaybeUnassignedHandle = I;
+                }
+            }
+            if (NextMaybeUnassignedSlot == End) {
+                NextMaybeUnassignedSlot = This->NextSlot;
+                NextMaybeUnassignedHandle = None;
+            }
+        }
+
+        /**
+         * Move Slot to next assigned slot and return true.
+         * Otherwise the end is reached and Slot is assigned int64_t::max and return false.
+         */
+        bool MoveToNextAssignedSlot() {
+            while (Slot != End) {
+                if (Slot + 1 < NextMaybeUnassignedSlot) {  // next unassigned slot not reached
+                    Slot = Slot + 1;
+                    return true;
+                }
+
+                if (NextMaybeUnassignedHandle == None) {  // reaching end
+                    Slot = End;
+                    NextMaybeUnassignedSlot = End;
+                    NextMaybeUnassignedHandle = None;
+                    return false;
+                }
+
+                if (NextMaybeUnassignedHandle != None) {  // maybe reaching the next unassigned slot
+                    This->Lanes.lock(NextMaybeUnassignedHandle);
+                    const bool IsAssigned = (Slot + 1 < This->Handles[NextMaybeUnassignedHandle].NextSlot);
+                    This->Lanes.unlock(NextMaybeUnassignedHandle);
+                    if (IsAssigned) {
+                        Slot = Slot + 1;
+                    }
+                    FindNextMaybeUnassignedSlot();
+                    if (IsAssigned) {
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+    };
+
+    using iterator = Iterator;
+
+    /// Initialize the datastructure with the given capacity.
+    ConcurrentFlyweight(const std::size_t LaneCount, const std::size_t InitialCapacity,
+            const bool ReserveFirst, const Hash& hash = Hash(), const KeyEqual& key_equal = KeyEqual(),
+            const KeyFactory& key_factory = KeyFactory())
+            : Lanes(LaneCount), HandleCount(LaneCount),
+              Mapping(LaneCount, InitialCapacity, hash, key_equal, key_factory) {
+        Slots = std::make_unique<const value_type*[]>(InitialCapacity);
+        Handles = std::make_unique<Handle[]>(HandleCount);
+        NextSlot = (ReserveFirst ? 1 : 0);
+        MaxSlotBeforeGrow = InitialCapacity - 1;
+    }
+
+    /// Initialize the datastructure with a capacity of 8 elements.
+    ConcurrentFlyweight(const std::size_t LaneCount, const bool ReserveFirst, const Hash& hash = Hash(),
+            const KeyEqual& key_equal = KeyEqual(), const KeyFactory& key_factory = KeyFactory())
+
+            : ConcurrentFlyweight(LaneCount, 8, ReserveFirst, hash, key_equal, key_factory) {}
+
+    /// Initialize the datastructure with a capacity of 8 elements.
+    ConcurrentFlyweight(const std::size_t LaneCount, const Hash& hash = Hash(),
+            const KeyEqual& key_equal = KeyEqual(), const KeyFactory& key_factory = KeyFactory())
+            : ConcurrentFlyweight(LaneCount, 8, false, hash, key_equal, key_factory) {}
+
+    virtual ~ConcurrentFlyweight() {
+        for (lane_id I = 0; I < HandleCount; ++I) {
+            if (Handles[I].NextNode) {
+                delete Handles[I].NextNode;
+            }
+        }
+    }
+
+    /**
+     * Change the number of lanes and possibly grow the number of handles.
+     * Do not use while threads are using this datastructure.
+     */
+    void setNumLanes(const std::size_t NumLanes) {
+        if (NumLanes > HandleCount) {
+            std::unique_ptr<Handle[]> NextHandles = std::make_unique<Handle[]>(NumLanes);
+            std::copy(Handles.get(), Handles.get() + HandleCount, NextHandles.get());
+            Handles.swap(NextHandles);
+            HandleCount = NumLanes;
+        }
+        Mapping.setNumLanes(NumLanes);
+        Lanes.setNumLanes(NumLanes);
+    }
+
+    /** Return a concurrent iterator on the first element. */
+    Iterator begin(const lane_id H) const {
+        return Iterator(this, H);
+    }
+
+    /** Return an iterator past the last element. */
+    Iterator end() const {
+        return Iterator(this);
+    }
+
+    /// Return true if the value is in the map.
+    template <typename K>
+    bool weakContains(const lane_id H, const K& X) const {
+        return Mapping.weakContains(H, X);
+    }
+
+    /// Return the value associated with the given index.
+    /// Assumption: the index is mapped in the datastructure.
+    const Key& fetch(const lane_id H, const index_type Idx) const {
+        const auto Lane = Lanes.guard(H);
+        return Slots[Idx]->first;
+    }
+
+    /// Return the pair of the index for the given value and a boolean
+    /// indicating if the value was already present (false) or inserted by this handle (true).
+    /// Insert the value and return a fresh index if the value is not
+    /// yet indexed.
+    template <class... Args>
+    std::pair<index_type, bool> findOrInsert(const lane_id H, Args&&... Xs) {
+        const auto Lane = Lanes.guard(H);
+        int64_t Slot = Handles[H].NextSlot;
+        node_type Node;
+
+        if (Slot == -1) {
+            // reserve a slot in the index, be it for now or later usage.
+            Slot = NextSlot++;
+            Node = Mapping.node(static_cast<index_type>(Slot));
+
+            Handles[H].NextSlot = Slot;
+            Handles[H].NextNode = Node;
+
+            if (Slot > MaxSlotBeforeGrow) {
+                tryGrow(H);
+            }
+        } else {
+            Node = Handles[H].NextNode;
+        }
+
+        // insert key in the index in advance
+        Slots[Slot] = &Node->value();
+
+        auto Res = Mapping.get(H, Node, std::forward<Args>(Xs)...);
+        if (Res.second) {
+            // inserted by self
+            Handles[H].NextSlot = -1;
+            Handles[H].NextNode = node_type{};
+            return std::make_pair(static_cast<index_type>(Slot), true);
+        } else {
+            // inserted concurrently by another handle,
+            return std::make_pair(Res.first->second, false);
+        }
+    }
+
+private:
+    using map_type = ConcurrentInsertOnlyHashMap<LanesPolicy, Key, index_type, Hash, KeyEqual, KeyFactory>;
+    using node_type = typename map_type::node_type;
+
+    struct Handle {
+        /// Slot where this handle will store its next value
+        int64_t NextSlot = -1;
+        node_type NextNode = nullptr;
+    };
+
+protected:
+    // The concurrency manager.
+    LanesPolicy Lanes;
+
+private:
+    // Number of handles
+    std::size_t HandleCount;
+
+    // Handle for each concurrent lane.
+    std::unique_ptr<Handle[]> Handles;
+
+    // Slots[I] points to the value associated with index I.
+    std::unique_ptr<const value_type*[]> Slots;
+
+    // The map from keys to index.
+    map_type Mapping;
+
+    // Next available slot.
+    std::atomic<std::int64_t> NextSlot;
+
+    // Maximum allowed slot index before growing
+    std::int64_t MaxSlotBeforeGrow;
+
+    bool tryGrow(const lane_id H) {
+        Lanes.beforeLockAllBut(H);
+
+        if (NextSlot <= MaxSlotBeforeGrow) {
+            // Current size is fine
+            Lanes.beforeUnlockAllBut(H);
+            return false;
+        }
+
+        Lanes.lockAllBut(H);
+
+        {  // safe section
+            const std::size_t CurrentSize = MaxSlotBeforeGrow + 1;
+            const std::size_t NewSize = (CurrentSize << 1);  // double size policy
+            std::unique_ptr<const value_type*[]> NewSlots = std::make_unique<const value_type*[]>(NewSize);
+            std::memcpy(NewSlots.get(), Slots.get(), sizeof(const value_type*) * CurrentSize);
+            Slots = std::move(NewSlots);
+            MaxSlotBeforeGrow = NewSize - 1;
+        }
+
+        Lanes.beforeUnlockAllBut(H);
+        Lanes.unlockAllBut(H);
+
+        return true;
+    }
+};
+
+#ifdef _OPENMP
+/** A Flyweight datastructure with concurrent access specialized for OpenMP. */
+template <class Key, class Hash = std::hash<Key>, class KeyEqual = std::equal_to<Key>,
+        class KeyFactory = details::Factory<Key>>
+class OmpFlyweight : protected ConcurrentFlyweight<ConcurrentLanes, Key, Hash, KeyEqual, KeyFactory> {
+public:
+    using Base = ConcurrentFlyweight<ConcurrentLanes, Key, Hash, KeyEqual, KeyFactory>;
+    using index_type = typename Base::index_type;
+    using lane_id = typename Base::lane_id;
+    using iterator = typename Base::iterator;
+
+    explicit OmpFlyweight(const std::size_t LaneCount, const std::size_t InitialCapacity = 8,
+            const bool ReserveFirst = false, const Hash& hash = Hash(),
+            const KeyEqual& key_equal = KeyEqual(), const KeyFactory& key_factory = KeyFactory())
+            : Base(LaneCount, InitialCapacity, ReserveFirst, hash, key_equal, key_factory) {}
+
+    ~OmpFlyweight() {}
+
+    iterator begin() const {
+        return Base::begin(Base::Lanes.threadLane());
+    }
+
+    iterator end() const {
+        return Base::end();
+    }
+
+    template <typename K>
+    bool weakContains(const K& X) const {
+        return Base::weakContains(Base::Lanes.threadLane(), X);
+    }
+
+    const Key& fetch(const index_type Idx) const {
+        return Base::fetch(Base::Lanes.threadLane(), Idx);
+    }
+
+    template <class... Args>
+    std::pair<index_type, bool> findOrInsert(Args&&... Xs) {
+        return Base::findOrInsert(Base::Lanes.threadLane(), std::forward<Args>(Xs)...);
+    }
+};
+#endif
+
+/**
+ * A Flyweight datastructure with sequential access.
+ *
+ * Reuse the concurrent flyweight with a single access handle.
+ */
+template <class Key, class Hash = std::hash<Key>, class KeyEqual = std::equal_to<Key>,
+        class KeyFactory = details::Factory<Key>>
+class SeqFlyweight : protected ConcurrentFlyweight<SeqConcurrentLanes, Key, Hash, KeyEqual, KeyFactory> {
+public:
+    using Base = ConcurrentFlyweight<SeqConcurrentLanes, Key, Hash, KeyEqual, KeyFactory>;
+    using index_type = typename Base::index_type;
+    using lane_id = typename Base::lane_id;
+    using iterator = typename Base::iterator;
+
+    explicit SeqFlyweight(const std::size_t NumLanes, const std::size_t InitialCapacity = 8,
+            const bool ReserveFirst = false, const Hash& hash = Hash(),
+            const KeyEqual& key_equal = KeyEqual(), const KeyFactory& key_factory = KeyFactory())
+            : Base(NumLanes, InitialCapacity, ReserveFirst, hash, key_equal, key_factory) {}
+
+    ~SeqFlyweight() {}
+
+    iterator begin() const {
+        return Base::begin(0);
+    }
+
+    iterator end() const {
+        return Base::end();
+    }
+
+    template <typename K>
+    bool weakContains(const K& X) const {
+        return Base::weakContains(0, X);
+    }
+
+    const Key& fetch(const index_type Idx) const {
+        return Base::fetch(0, Idx);
+    }
+
+    template <class... Args>
+    std::pair<index_type, bool> findOrInsert(Args&&... Xs) {
+        return Base::findOrInsert(0, std::forward<Args>(Xs)...);
+    }
+};
+
+#ifdef _OPENMP
+template <class Key, class Hash = std::hash<Key>, class KeyEqual = std::equal_to<Key>,
+        class KeyFactory = details::Factory<Key>>
+using FlyweightImpl = OmpFlyweight<Key, Hash, KeyEqual, KeyFactory>;
+#else
+template <class Key, class Hash = std::hash<Key>, class KeyEqual = std::equal_to<Key>,
+        class KeyFactory = details::Factory<Key>>
+using FlyweightImpl = SeqFlyweight<Key, Hash, KeyEqual, KeyFactory>;
+#endif
+
+}  // namespace souffle
diff --git a/cbits/souffle/datastructure/ConcurrentInsertOnlyHashMap.h b/cbits/souffle/datastructure/ConcurrentInsertOnlyHashMap.h
new file mode 100644
--- /dev/null
+++ b/cbits/souffle/datastructure/ConcurrentInsertOnlyHashMap.h
@@ -0,0 +1,458 @@
+/*
+ * Souffle - A Datalog Compiler
+ * Copyright (c) 2021, The Souffle Developers. All rights reserved
+ * Licensed under the Universal Permissive License v 1.0 as shown at:
+ * - https://opensource.org/licenses/UPL
+ * - <souffle root>/licenses/SOUFFLE-UPL.txt
+ */
+#pragma once
+
+#include "souffle/utility/ParallelUtil.h"
+
+#include <array>
+#include <atomic>
+#include <cassert>
+#include <cmath>
+#include <memory>
+#include <mutex>
+#include <vector>
+
+namespace souffle {
+namespace details {
+
+static const std::vector<std::pair<unsigned, unsigned>> ToPrime = {
+        // https://primes.utm.edu/lists/2small/0bit.html
+        // ((2^n) - k) is prime
+        // {n, k}
+        {4, 3},  // 2^4 - 3 = 13
+        {8, 5},  // 8^5 - 5 = 251
+        {9, 3}, {10, 3}, {11, 9}, {12, 3}, {13, 1}, {14, 3}, {15, 19}, {16, 15}, {17, 1}, {18, 5}, {19, 1},
+        {20, 3}, {21, 9}, {22, 3}, {23, 15}, {24, 3}, {25, 39}, {26, 5}, {27, 39}, {28, 57}, {29, 3},
+        {30, 35}, {31, 1}, {32, 5}, {33, 9}, {34, 41}, {35, 31}, {36, 5}, {37, 25}, {38, 45}, {39, 7},
+        {40, 87}, {41, 21}, {42, 11}, {43, 57}, {44, 17}, {45, 55}, {46, 21}, {47, 115}, {48, 59}, {49, 81},
+        {50, 27}, {51, 129}, {52, 47}, {53, 111}, {54, 33}, {55, 55}, {56, 5}, {57, 13}, {58, 27}, {59, 55},
+        {60, 93}, {61, 1}, {62, 57}, {63, 25}};
+
+// (2^64)-59 is the largest prime that fits in uint64_t
+static constexpr uint64_t LargestPrime64 = 18446744073709551557UL;
+
+// Return a prime greater or equal to the lower bound.
+// Return 0 if the next prime would not fit in 64 bits.
+static uint64_t GreaterOrEqualPrime(const uint64_t LowerBound) {
+    if (LowerBound > LargestPrime64) {
+        return 0;
+    }
+
+    for (std::size_t I = 0; I < ToPrime.size(); ++I) {
+        const uint64_t N = ToPrime[I].first;
+        const uint64_t K = ToPrime[I].second;
+        const uint64_t Prime = (1UL << N) - K;
+        if (Prime >= LowerBound) {
+            return Prime;
+        }
+    }
+    return LargestPrime64;
+}
+
+template <typename T>
+struct Factory {
+    template <class... Args>
+    T& replace(T& Place, Args&&... Xs) {
+        Place = T{std::forward<Args>(Xs)...};
+        return Place;
+    }
+};
+
+}  // namespace details
+
+/**
+ * A concurrent, almost lock-free associative hash-map that can only grow.
+ * Elements cannot be removed, the hash-map can only grow.
+ *
+ * The datastructures enables a configurable number of concurrent access lanes.
+ * Access to the datastructure is lock-free between different lanes.
+ * Concurrent accesses through the same lane is sequential.
+ *
+ * Growing the datastructure requires to temporarily lock all lanes to let a
+ * single lane perform the growing operation. The global lock is amortized
+ * thanks to an exponential growth strategy.
+ */
+template <class LanesPolicy, class Key, class T, class Hash = std::hash<Key>,
+        class KeyEqual = std::equal_to<Key>, class KeyFactory = details::Factory<Key>>
+class ConcurrentInsertOnlyHashMap {
+public:
+    class Node;
+
+    using key_type = Key;
+    using mapped_type = T;
+    using node_type = Node*;
+    using value_type = std::pair<const Key, const T>;
+    using size_type = std::size_t;
+    using hasher = Hash;
+    using key_equal = KeyEqual;
+    using self_type = ConcurrentInsertOnlyHashMap<Key, T, Hash, KeyEqual, KeyFactory>;
+    using lane_id = typename LanesPolicy::lane_id;
+
+    class Node {
+    public:
+        virtual ~Node() {}
+        virtual const value_type& value() const = 0;
+        virtual const key_type& key() const = 0;
+        virtual const mapped_type& mapped() const = 0;
+    };
+
+private:
+    // Each bucket of the hash-map is a linked list.
+    struct BucketList : Node {
+        virtual ~BucketList() {}
+
+        BucketList(const Key& K, const T& V, BucketList* N) : Value(K, V), Next(N) {}
+
+        const value_type& value() const {
+            return Value;
+        }
+
+        const key_type& key() const {
+            return Value.first;
+        }
+
+        const mapped_type& mapped() const {
+            return Value.second;
+        }
+
+        // Stores the couple of a key and its associated value.
+        value_type Value;
+
+        // Points to next element of the map that falls into the same bucket.
+        BucketList* Next;
+    };
+
+public:
+    /**
+     * @brief Construct a hash-map with at least the given number of buckets.
+     *
+     * Load-factor is initialized to 1.0.
+     */
+    ConcurrentInsertOnlyHashMap(const std::size_t LaneCount, const std::size_t Bucket_Count,
+            const Hash& hash = Hash(), const KeyEqual& key_equal = KeyEqual(),
+            const KeyFactory& key_factory = KeyFactory())
+            : Lanes(LaneCount), Hasher(hash), EqualTo(key_equal), Factory(key_factory) {
+        Size = 0;
+        BucketCount = details::GreaterOrEqualPrime(Bucket_Count);
+        if (BucketCount == 0) {
+            // Hopefuly this number of buckets is never reached.
+            BucketCount = std::numeric_limits<std::size_t>::max();
+        }
+        LoadFactor = 1.0;
+        Buckets = std::make_unique<std::atomic<BucketList*>[]>(BucketCount);
+        MaxSizeBeforeGrow = std::ceil(LoadFactor * BucketCount);
+    }
+
+    ConcurrentInsertOnlyHashMap(const Hash& hash = Hash(), const KeyEqual& key_equal = KeyEqual(),
+            const KeyFactory& key_factory = KeyFactory())
+            : ConcurrentInsertOnlyHashMap(8, hash, key_equal, key_factory) {}
+
+    ~ConcurrentInsertOnlyHashMap() {
+        for (std::size_t Bucket = 0; Bucket < BucketCount; ++Bucket) {
+            BucketList* L = Buckets[Bucket].load(std::memory_order_relaxed);
+            while (L != nullptr) {
+                BucketList* BL = L;
+                L = L->Next;
+                delete (BL);
+            }
+        }
+    }
+
+    void setNumLanes(const std::size_t NumLanes) {
+        Lanes.setNumLanes(NumLanes);
+    }
+
+    /** @brief Create a fresh node initialized with the given value and a
+     * default-constructed key.
+     *
+     * The ownership of the returned node given to the caller.
+     */
+    node_type node(const T& V) {
+        BucketList* BL = new BucketList(Key{}, V, nullptr);
+        return static_cast<node_type>(BL);
+    }
+
+    /** @brief Checks if the map contains an element with the given key.
+     *
+     * The search is done concurrently with possible insertion of the
+     * searched key. If return true, then there is definitely an element
+     * with the specified key, if return false then there was no such
+     * element when the search began.
+     */
+    template <class K>
+    bool weakContains(const lane_id H, const K& X) const {
+        const size_t HashValue = Hasher(X);
+        const auto Guard = Lanes.guard(H);
+        const size_t Bucket = HashValue % BucketCount;
+
+        BucketList* L = Buckets[Bucket].load(std::memory_order_consume);
+        while (L != nullptr) {
+            if (EqualTo(L->Value.first, X)) {
+                // found the key
+                return true;
+            }
+            L = L->Next;
+        }
+        return false;
+    }
+
+    /**
+     * @brief Inserts in-place if the key is not mapped, does nothing if the key already exists.
+     *
+     * @param H is the access lane.
+     *
+     * @param N is a node initialized with the mapped value to insert.
+     *
+     * @param Xs are arguments to forward to the hasher, the comparator and and
+     * the constructor of the key.
+     *
+     *
+     * Be Careful: the inserted node becomes available to concurrent lanes as
+     * soon as it is inserted, thus concurrent lanes may access the inserted
+     * value even before the inserting lane returns from this function.
+     * This is the reason why the inserting lane must prepare the inserted
+     * node's mapped value prior to calling this function.
+     *
+     * Be Careful: the given node remains the ownership of the caller unless
+     * the returned couple second member is true.
+     *
+     * Be Careful: the given node may not be inserted if the key already
+     * exists.  The caller is in charge of handling that case and either
+     * dispose of the node or save it for the next insertion operation.
+     *
+     * Be Careful: Once the given node is actually inserted, its ownership is
+     * transfered to the hash-map. However it remains valid.
+     *
+     * If the key that compares equal to arguments Xs exists, then nothing is
+     * inserted. The returned value is the couple of the pointer to the
+     * existing value and the false boolean value.
+     *
+     * If the key that compares equal to arguments Xs does not exist, then the
+     * node N is updated with the key constructed from Xs, and inserted in the
+     * hash-map. The returned value is the couple of the pointer to the
+     * inserted value and the true boolean value.
+     *
+     */
+    template <class... Args>
+    std::pair<const value_type*, bool> get(const lane_id H, node_type N, Args&&... Xs) {
+        // At any time a concurrent lane may insert the key before this lane.
+        //
+        // The synchronisation point is the atomic compare-and-exchange of the
+        // head of the bucket list that must contain the inserted node.
+        //
+        // The insertion algorithm is as follow:
+        //
+        // 1) Compute the key hash from Xs.
+        //
+        // 2) Lock the lane, that also prevent concurrent lanes from growing of
+        // the datastructure.
+        //
+        // 3) Determine the bucket where the element must be inserted.
+        //
+        // 4) Read the "last known head" of the bucket list. Other lanes
+        // inserting in the same bucket may update the bucket head
+        // concurrently.
+        //
+        // 5) Search the bucket list for the key by comparing with Xs starting
+        // from the last known head. If it is not the first round of search,
+        // then stop searching where the previous round of search started.
+        //
+        // 6) If the key is found return the couple of the value pointer and
+        // false (to indicate that this lane did not insert the node N).
+        //
+        // 7) It the key is not found prepare N for insertion by updating its
+        // key with Xs and chaining the last known head.
+        //
+        // 8) Try to exchange to last known head with N at the bucket head. The
+        // atomic compare and exchange operation guarantees that it only
+        // succeed if not other node was inserted in the bucket since we
+        // searched it, otherwise it fails when another lane has concurrently
+        // inserted a node in the same bucket.
+        //
+        // 9) If the atomic compare and exchange succeeded, the node has just
+        // been inserted by this lane. From now-on other lanes can also see
+        // the node. Return the couple of a pointer to the inserted value and
+        // the true boolean.
+        //
+        // 10) If the atomic compare and exchange failed, another node has been
+        // inserted by a concurrent lane in the same bucket. A new round of
+        // search is required -> restart from step 4.
+        //
+        //
+        // The datastructure is optionaly grown after step 9) before returning.
+
+        const value_type* Value = nullptr;
+        bool Inserted = false;
+
+        size_t NewSize;
+
+        // 1)
+        const size_t HashValue = Hasher(std::forward<Args>(Xs)...);
+
+        // 2)
+        Lanes.lock(H);  // prevent the datastructure from growing
+
+        // 3)
+        const size_t Bucket = HashValue % BucketCount;
+
+        // 4)
+        // the head of the bucket's list last time we checked
+        BucketList* LastKnownHead = Buckets[Bucket].load(std::memory_order_relaxed);
+        // the head of the bucket's list we already searched from
+        BucketList* SearchedFrom = nullptr;
+        // the node we want to insert
+        BucketList* Node = static_cast<BucketList*>(N);
+
+        // Loop until either the node is inserted or the key is found in the bucket.
+        // Assuming bucket collisions are rare this loop is not executed more than once.
+        while (true) {
+            // 5)
+            // search the key in the bucket, stop where we already search at a
+            // previous iteration.
+            BucketList* L = LastKnownHead;
+            while (L != SearchedFrom) {
+                if (EqualTo(L->Value.first, std::forward<Args>(Xs)...)) {
+                    // 6)
+                    // found the key
+                    Value = &(L->Value);
+                    goto Done;
+                }
+                L = L->Next;
+            }
+            SearchedFrom = LastKnownHead;
+
+            // 7)
+            // Not found in bucket, prepare node chaining.
+            Node->Next = LastKnownHead;
+            // The factory step could be done only once, but assuming bucket collisions are
+            // rare this whole loop is not executed more than once.
+            Factory.replace(const_cast<key_type&>(Node->Value.first), std::forward<Args>(Xs)...);
+
+            // 8)
+            // Try to insert the key in front of the bucket's list.
+            // This operation also performs step 4) because LastKnownHead is
+            // updated in the process.
+            if (Buckets[Bucket].compare_exchange_strong(
+                        LastKnownHead, Node, std::memory_order_release, std::memory_order_relaxed)) {
+                // 9)
+                Inserted = true;
+                NewSize = ++Size;
+                Value = &(Node->Value);
+                Node = nullptr;
+                goto AfterInserted;
+            }
+
+            // 10) concurrent insertion detected in this bucket, new round required.
+        }
+
+    AfterInserted : {
+        if (NewSize > MaxSizeBeforeGrow) {
+            tryGrow(H);
+        }
+    }
+
+    Done:
+
+        Lanes.unlock(H);
+
+        // 6,9)
+        return std::make_pair(Value, Inserted);
+    }
+
+private:
+    // The concurrent lanes manager.
+    LanesPolicy Lanes;
+
+    /// Hash function.
+    Hash Hasher;
+
+    /// Current number of buckets.
+    std::size_t BucketCount;
+
+    /// Atomic pointer to head bucket linked-list head.
+    std::unique_ptr<std::atomic<BucketList*>[]> Buckets;
+
+    /// The Equal-to function.
+    KeyEqual EqualTo;
+
+    KeyFactory Factory;
+
+    /// Current number of elements stored in the map.
+    std::atomic<std::size_t> Size;
+
+    /// Maximum size before the map should grow.
+    std::size_t MaxSizeBeforeGrow;
+
+    /// The load-factor of the map.
+    double LoadFactor;
+
+    // Grow the datastructure.
+    // Must be called while owning lane H.
+    bool tryGrow(const lane_id H) {
+        Lanes.beforeLockAllBut(H);
+
+        if (Size <= MaxSizeBeforeGrow) {
+            // Current size is fine
+            Lanes.beforeUnlockAllBut(H);
+            return false;
+        }
+
+        Lanes.lockAllBut(H);
+
+        {  // safe section
+
+            // Compute the new number of buckets:
+            // Chose a prime number of buckets that ensures the desired load factor
+            // given the current number of elements in the map.
+            const std::size_t CurrentSize = Size;
+            const std::size_t NeededBucketCount = std::ceil(CurrentSize / LoadFactor);
+            std::size_t NewBucketCount = NeededBucketCount;
+            for (std::size_t I = 0; I < details::ToPrime.size(); ++I) {
+                const uint64_t N = details::ToPrime[I].first;
+                const uint64_t K = details::ToPrime[I].second;
+                const uint64_t Prime = (1UL << N) - K;
+                if (Prime >= NeededBucketCount) {
+                    NewBucketCount = Prime;
+                    break;
+                }
+            }
+
+            std::unique_ptr<std::atomic<BucketList*>[]> NewBuckets =
+                    std::make_unique<std::atomic<BucketList*>[]>(NewBucketCount);
+
+            // Rehash, this operation is costly because it requires to scan
+            // the existing elements, compute its hash to find its new bucket
+            // and insert in the new bucket.
+            //
+            // Maybe concurrent lanes could help using some job-stealing algorithm.
+            for (std::size_t B = 0; B < BucketCount; ++B) {
+                BucketList* L = Buckets[B].load(std::memory_order_relaxed);
+                while (L) {
+                    BucketList* const Elem = L;
+                    L = L->Next;
+
+                    const auto& Value = Elem->Value;
+                    std::size_t NewHash = Hasher(Value.first);
+                    const std::size_t NewBucket = NewHash % NewBucketCount;
+                    Elem->Next = NewBuckets[NewBucket].load(std::memory_order_relaxed);
+                    NewBuckets[NewBucket].store(Elem, std::memory_order_relaxed);
+                }
+            }
+
+            Buckets = std::move(NewBuckets);
+            BucketCount = NewBucketCount;
+            MaxSizeBeforeGrow = (NewBucketCount * LoadFactor);
+        }
+
+        Lanes.beforeUnlockAllBut(H);
+        Lanes.unlockAllBut(H);
+        return true;
+    }
+};
+
+}  // namespace souffle
diff --git a/cbits/souffle/datastructure/EquivalenceRelation.h b/cbits/souffle/datastructure/EquivalenceRelation.h
--- a/cbits/souffle/datastructure/EquivalenceRelation.h
+++ b/cbits/souffle/datastructure/EquivalenceRelation.h
@@ -117,8 +117,8 @@
             for (auto& p : it) {
                 value_type rep = p.first;
                 StatesList& pl = *p.second;
-                const size_t ksize = pl.size();
-                for (size_t i = 0; i < ksize; ++i) {
+                const std::size_t ksize = pl.size();
+                for (std::size_t i = 0; i < ksize; ++i) {
                     this->sds.unionNodes(rep, pl.get(i));
                 }
             }
@@ -192,6 +192,10 @@
         return contains(tuple[0], tuple[1]);
     };
 
+    bool contains(const TupleType& tuple) const {
+        return contains(tuple[0], tuple[1]);
+    };
+
     void emptyPartition() const {
         // delete the beautiful values inside (they're raw ptrs, so they need to be.)
         for (auto& pair : equivalencePartition) {
@@ -219,14 +223,14 @@
      * Size of relation
      * @return the sum of the number of pairs per disjoint set
      */
-    size_t size() const {
+    std::size_t size() const {
         genAllDisjointSetLists();
 
         statesLock.lock_shared();
 
-        size_t retVal = 0;
+        std::size_t retVal = 0;
         for (auto& e : this->equivalencePartition) {
-            const size_t s = e.second->size();
+            const std::size_t s = e.second->size();
             retVal += s * s;
         }
 
@@ -241,10 +245,10 @@
     class iterator {
     public:
         typedef std::forward_iterator_tag iterator_category;
-        typedef TupleType value_type;
-        typedef ptrdiff_t difference_type;
-        typedef value_type* pointer;
-        typedef value_type& reference;
+        using value_type = TupleType;
+        using difference_type = ptrdiff_t;
+        using pointer = value_type*;
+        using reference = value_type&;
 
         // one iterator for signalling the end (simplifies)
         explicit iterator(const EquivalenceRelation* br, bool /* signalIsEndIterator */)
@@ -447,9 +451,9 @@
         typename StatesMap::iterator djSetMapListEnd;
 
         // used for ALL, and POSTERIOR (just a current index in the cList)
-        size_t cAnteriorIndex = 0;
+        std::size_t cAnteriorIndex = 0;
         // used for ALL, and ANTERIOR (just a current index in the cList)
-        size_t cPosteriorIndex = 0;
+        std::size_t cPosteriorIndex = 0;
     };
 
 public:
@@ -561,6 +565,11 @@
         return end();
     }
 
+    iterator lower_bound(const TupleType& entry) const {
+        operation_hints hints;
+        return lower_bound(entry, hints);
+    }
+
     /**
      * This function is only here in order to unify interfaces in InterpreterIndex.
      * Unlike the name suggestes, it omit the arguments and simply return the end
@@ -573,6 +582,11 @@
         return end();
     }
 
+    iterator upper_bound(const TupleType& entry) const {
+        operation_hints hints;
+        return upper_bound(entry, hints);
+    }
+
     /**
      * Check emptiness.
      */
@@ -639,11 +653,11 @@
      * @param chunks the number of requested partitions
      * @return a list of the iterators as ranges
      */
-    std::vector<souffle::range<iterator>> partition(size_t chunks) const {
+    std::vector<souffle::range<iterator>> partition(std::size_t chunks) const {
         // generate all reps
         genAllDisjointSetLists();
 
-        size_t numPairs = this->size();
+        std::size_t numPairs = this->size();
         if (numPairs == 0) return {};
         if (numPairs == 1 || chunks <= 1) return {souffle::make_range(begin(), end())};
 
@@ -659,9 +673,9 @@
         // keep it simple stupid
         // just go through and if the size of the binrel is > numpairs/chunks, then generate an anteriorIt for
         // each
-        const size_t perchunk = numPairs / chunks;
+        const std::size_t perchunk = numPairs / chunks;
         for (const auto& itp : equivalencePartition) {
-            const size_t s = itp.second->size();
+            const std::size_t s = itp.second->size();
             if (s * s > perchunk) {
                 for (const auto& i : *itp.second) {
                     ret.push_back(souffle::make_range(anteriorIt(i), end()));
@@ -717,8 +731,8 @@
         // btree version
         emptyPartition();
 
-        size_t dSetSize = this->sds.ds.a_blocks.size();
-        for (size_t i = 0; i < dSetSize; ++i) {
+        std::size_t dSetSize = this->sds.ds.a_blocks.size();
+        for (std::size_t i = 0; i < dSetSize; ++i) {
             typename TupleType::value_type sparseVal = this->sds.toSparse(i);
             parent_t rep = this->sds.findNode(sparseVal);
 
diff --git a/cbits/souffle/datastructure/PiggyList.h b/cbits/souffle/datastructure/PiggyList.h
--- a/cbits/souffle/datastructure/PiggyList.h
+++ b/cbits/souffle/datastructure/PiggyList.h
@@ -3,6 +3,7 @@
 #include "souffle/utility/ParallelUtil.h"
 #include <array>
 #include <atomic>
+#include <cassert>
 #include <cstring>
 #include <iostream>
 #include <iterator>
@@ -12,7 +13,7 @@
  * Some versions of MSVC do not provide a builtin for counting leading zeroes
  * like gcc, so we have to implement it ourselves.
  */
-#if _MSC_VER < 1924
+#if defined(_MSC_VER)
 unsigned long __inline __builtin_clzll(unsigned long long value) {
     unsigned long msb = 0;
 
@@ -21,7 +22,7 @@
     else
         return 64;
 }
-#endif  // _MSC_VER < 1924
+#endif  // _MSC_VER
 #endif  // _WIN32
 
 using std::size_t;
@@ -38,17 +39,17 @@
     RandomInsertPiggyList() = default;
     // an instance where the initial size is not 65k, and instead is user settable (to a power of
     // initialbitsize)
-    RandomInsertPiggyList(size_t initialbitsize) : BLOCKBITS(initialbitsize) {}
+    RandomInsertPiggyList(std::size_t initialbitsize) : BLOCKBITS(initialbitsize) {}
 
     /** copy constructor */
     RandomInsertPiggyList(const RandomInsertPiggyList& other) : BLOCKBITS(other.BLOCKBITS) {
         this->numElements.store(other.numElements.load());
 
         // copy blocks from the old lookup table to this one
-        for (size_t i = 0; i < maxContainers; ++i) {
+        for (std::size_t i = 0; i < maxContainers; ++i) {
             if (other.blockLookupTable[i].load() != nullptr) {
                 // calculate the size of that block
-                const size_t blockSize = INITIALBLOCKSIZE << i;
+                const std::size_t blockSize = INITIALBLOCKSIZE << i;
 
                 // allocate that in the new container
                 this->blockLookupTable[i].store(new T[blockSize]);
@@ -71,25 +72,25 @@
         freeList();
     }
 
-    inline size_t size() const {
+    inline std::size_t size() const {
         return numElements.load();
     }
 
-    inline T* getBlock(size_t blockNum) const {
+    inline T* getBlock(std::size_t blockNum) const {
         return blockLookupTable[blockNum];
     }
 
-    inline T& get(size_t index) const {
-        size_t nindex = index + INITIALBLOCKSIZE;
-        size_t blockNum = (63 - __builtin_clzll(nindex));
-        size_t blockInd = (nindex) & ((1 << blockNum) - 1);
+    inline T& get(std::size_t index) const {
+        std::size_t nindex = index + INITIALBLOCKSIZE;
+        std::size_t blockNum = (63 - __builtin_clzll(nindex));
+        std::size_t blockInd = (nindex) & ((1 << blockNum) - 1);
         return this->getBlock(blockNum - BLOCKBITS)[blockInd];
     }
 
-    void insertAt(size_t index, T value) {
+    void insertAt(std::size_t index, T value) {
         // starting with an initial blocksize requires some shifting to transform into a nice powers of two
         // series
-        size_t blockNum = (63 - __builtin_clzll(index + INITIALBLOCKSIZE)) - BLOCKBITS;
+        std::size_t blockNum = (63 - __builtin_clzll(index + INITIALBLOCKSIZE)) - BLOCKBITS;
 
         // allocate the block if not allocated
         if (blockLookupTable[blockNum].load() == nullptr) {
@@ -110,14 +111,14 @@
         freeList();
         numElements.store(0);
     }
-    const size_t BLOCKBITS = 16ul;
-    const size_t INITIALBLOCKSIZE = (1ul << BLOCKBITS);
+    const std::size_t BLOCKBITS = 16ul;
+    const std::size_t INITIALBLOCKSIZE = (1ul << BLOCKBITS);
 
     // number of elements currently stored within
-    std::atomic<size_t> numElements{0};
+    std::atomic<std::size_t> numElements{0};
 
     // 2^64 - 1 elements can be stored (default initialised to nullptrs)
-    static constexpr size_t maxContainers = 64;
+    static constexpr std::size_t maxContainers = 64;
     std::array<std::atomic<T*>, maxContainers> blockLookupTable = {};
 
     // for parallel node insertions
@@ -129,7 +130,7 @@
     void freeList() {
         slock.lock();
         // delete all - deleting a nullptr is a no-op
-        for (size_t i = 0; i < maxContainers; ++i) {
+        for (std::size_t i = 0; i < maxContainers; ++i) {
             delete[] blockLookupTable[i].load();
             // reset the container within to be empty.
             blockLookupTable[i].store(nullptr);
@@ -142,7 +143,7 @@
 class PiggyList {
 public:
     PiggyList() : num_containers(0), container_size(0), m_size(0) {}
-    PiggyList(size_t initialbitsize)
+    PiggyList(std::size_t initialbitsize)
             : BLOCKBITS(initialbitsize), num_containers(0), container_size(0), m_size(0) {}
 
     /** copy constructor */
@@ -152,8 +153,8 @@
         m_size.store(other.m_size.load());
         // copy each chunk from other into this
         // the size of the next container to allocate
-        size_t cSize = BLOCKSIZE;
-        for (size_t i = 0; i < other.num_containers; ++i) {
+        std::size_t cSize = BLOCKSIZE;
+        for (std::size_t i = 0; i < other.num_containers; ++i) {
             this->blockLookupTable[i] = new T[cSize];
             std::memcpy(this->blockLookupTable[i], other.blockLookupTable[i], cSize * sizeof(T));
             cSize <<= 1;
@@ -177,16 +178,16 @@
      *  that haven't had time to had containers created and updated
      * @return the number of nodes exist within the list + number of nodes queued to be inserted
      */
-    inline size_t size() const {
+    inline std::size_t size() const {
         return m_size.load();
     };
 
-    inline T* getBlock(size_t blocknum) const {
+    inline T* getBlock(std::size_t blocknum) const {
         return this->blockLookupTable[blocknum];
     }
 
-    size_t append(T element) {
-        size_t new_index = m_size.fetch_add(1, std::memory_order_acquire);
+    std::size_t append(T element) {
+        std::size_t new_index = m_size.fetch_add(1, std::memory_order_acquire);
 
         // will this not fit?
         if (container_size < new_index + 1) {
@@ -206,8 +207,8 @@
         return new_index;
     }
 
-    size_t createNode() {
-        size_t new_index = m_size.fetch_add(1, std::memory_order_acquire);
+    std::size_t createNode() {
+        std::size_t new_index = m_size.fetch_add(1, std::memory_order_acquire);
 
         // will this not fit?
         if (container_size < new_index + 1) {
@@ -231,11 +232,11 @@
      * @param index position to search
      * @return the value at index
      */
-    inline T& get(size_t index) const {
+    inline T& get(std::size_t index) const {
         // supa fast 2^16 size first block
-        size_t nindex = index + BLOCKSIZE;
-        size_t blockNum = (63 - __builtin_clzll(nindex));
-        size_t blockInd = (nindex) & ((1 << blockNum) - 1);
+        std::size_t nindex = index + BLOCKSIZE;
+        std::size_t blockNum = (63 - __builtin_clzll(nindex));
+        std::size_t blockInd = (nindex) & ((1 << blockNum) - 1);
         return this->getBlock(blockNum - BLOCKBITS)[blockInd];
     }
 
@@ -252,7 +253,7 @@
     }
 
     class iterator : std::iterator<std::forward_iterator_tag, T> {
-        size_t cIndex = 0;
+        std::size_t cIndex = 0;
         PiggyList* bl;
 
     public:
@@ -262,7 +263,7 @@
         /* begin iterator for iterating over all elements */
         iterator(PiggyList* bl) : bl(bl){};
         /* ender iterator for marking the end of the iteration */
-        iterator(PiggyList* bl, size_t beginInd) : cIndex(beginInd), bl(bl){};
+        iterator(PiggyList* bl, std::size_t beginInd) : cIndex(beginInd), bl(bl){};
 
         T operator*() {
             return bl->get(cIndex);
@@ -297,17 +298,17 @@
     iterator end() {
         return iterator(this, size());
     }
-    const size_t BLOCKBITS = 16ul;
-    const size_t BLOCKSIZE = (1ul << BLOCKBITS);
+    const std::size_t BLOCKBITS = 16ul;
+    const std::size_t BLOCKSIZE = (1ul << BLOCKBITS);
 
     // number of inserted
-    std::atomic<size_t> num_containers = 0;
-    size_t allocsize = BLOCKSIZE;
-    std::atomic<size_t> container_size = 0;
-    std::atomic<size_t> m_size = 0;
+    std::atomic<std::size_t> num_containers = 0;
+    std::size_t allocsize = BLOCKSIZE;
+    std::atomic<std::size_t> container_size = 0;
+    std::atomic<std::size_t> m_size = 0;
 
     // > 2^64 elements can be stored (default initialise to nullptrs)
-    static constexpr size_t max_conts = 64;
+    static constexpr std::size_t max_conts = 64;
     std::array<T*, max_conts> blockLookupTable = {};
 
     // for parallel node insertions
@@ -319,7 +320,7 @@
     void freeList() {
         sl.lock();
         // we don't know which ones are taken up!
-        for (size_t i = 0; i < num_containers; ++i) {
+        for (std::size_t i = 0; i < num_containers; ++i) {
             delete[] blockLookupTable[i];
         }
         sl.unlock();
diff --git a/cbits/souffle/datastructure/UnionFind.h b/cbits/souffle/datastructure/UnionFind.h
--- a/cbits/souffle/datastructure/UnionFind.h
+++ b/cbits/souffle/datastructure/UnionFind.h
@@ -18,6 +18,7 @@
 
 #include "souffle/datastructure/LambdaBTree.h"
 #include "souffle/datastructure/PiggyList.h"
+#include "souffle/utility/MiscUtil.h"
 #include <atomic>
 #include <cstddef>
 #include <cstdint>
@@ -67,7 +68,7 @@
     /**
      * Return the number of elements in this disjoint set (not the number of pairs)
      */
-    inline size_t size() {
+    inline std::size_t size() {
         auto sz = a_blocks.size();
         return sz;
     };
@@ -191,7 +192,7 @@
      */
     inline block_t makeNode() {
         // make node and find out where we've added it
-        size_t nodeDetails = a_blocks.createNode();
+        std::size_t nodeDetails = a_blocks.createNode();
 
         a_blocks.get(nodeDetails).store(pr2b(nodeDetails, 0));
 
diff --git a/cbits/souffle/io/IOSystem.h b/cbits/souffle/io/IOSystem.h
--- a/cbits/souffle/io/IOSystem.h
+++ b/cbits/souffle/io/IOSystem.h
@@ -1,6 +1,6 @@
 /*
  * Souffle - A Datalog Compiler
- * Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved
+ * Copyright (c) 2021, The Souffle Developers. All rights reserved
  * Licensed under the Universal Permissive License v 1.0 as shown at:
  * - https://opensource.org/licenses/UPL
  * - <souffle root>/licenses/SOUFFLE-UPL.txt
@@ -15,6 +15,7 @@
 #pragma once
 
 #include "souffle/RamTypes.h"
+#include "souffle/RecordTable.h"
 #include "souffle/SymbolTable.h"
 #include "souffle/io/ReadStream.h"
 #include "souffle/io/ReadStreamCSV.h"
@@ -34,7 +35,6 @@
 #include <string>
 
 namespace souffle {
-class RecordTable;
 
 class IOSystem {
 public:
diff --git a/cbits/souffle/io/ReadStream.h b/cbits/souffle/io/ReadStream.h
--- a/cbits/souffle/io/ReadStream.h
+++ b/cbits/souffle/io/ReadStream.h
@@ -1,6 +1,6 @@
 /*
  * Souffle - A Datalog Compiler
- * Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved
+ * Copyright (c) 2021, The Souffle Developers. All rights reserved
  * Licensed under the Universal Permissive License v 1.0 as shown at:
  * - https://opensource.org/licenses/UPL
  * - <souffle root>/licenses/SOUFFLE-UPL.txt
@@ -42,8 +42,6 @@
 public:
     template <typename T>
     void readAll(T& relation) {
-        auto lease = symbolTable.acquireLock();
-        (void)lease;
         while (const auto next = readNextTuple()) {
             const RamDomain* ramDomain = next.get();
             relation.insert(ramDomain);
@@ -60,9 +58,9 @@
      * @param consumed - if not nullptr: number of characters read.
      *
      */
-    RamDomain readRecord(const std::string& source, const std::string& recordTypeName, size_t pos = 0,
-            size_t* charactersRead = nullptr) {
-        const size_t initial_position = pos;
+    RamDomain readRecord(const std::string& source, const std::string& recordTypeName, std::size_t pos = 0,
+            std::size_t* charactersRead = nullptr) {
+        const std::size_t initial_position = pos;
 
         // Check if record type information are present
         auto&& recordInfo = types["records"][recordTypeName];
@@ -80,15 +78,15 @@
         }
 
         auto&& recordTypes = recordInfo["types"];
-        const size_t recordArity = recordInfo["arity"].long_value();
+        const std::size_t recordArity = recordInfo["arity"].long_value();
 
         std::vector<RamDomain> recordValues(recordArity);
 
         consumeChar(source, '[', pos);
 
-        for (size_t i = 0; i < recordArity; ++i) {
+        for (std::size_t i = 0; i < recordArity; ++i) {
             const std::string& recordType = recordTypes[i].string_value();
-            size_t consumed = 0;
+            std::size_t consumed = 0;
 
             if (i > 0) {
                 consumeChar(source, ',', pos);
@@ -96,7 +94,7 @@
             consumeWhiteSpace(source, pos);
             switch (recordType[0]) {
                 case 's': {
-                    recordValues[i] = symbolTable.unsafeLookup(readUntil(source, ",]", pos, &consumed));
+                    recordValues[i] = symbolTable.encode(readSymbol(source, ",]", pos, &consumed));
                     break;
                 }
                 case 'i': {
@@ -132,11 +130,14 @@
         return recordTable.pack(recordValues.data(), recordValues.size());
     }
 
-    RamDomain readADT(const std::string& source, const std::string& adtName, size_t pos = 0,
-            size_t* charactersRead = nullptr) {
-        const size_t initial_position = pos;
+    RamDomain readADT(const std::string& source, const std::string& adtName, std::size_t pos = 0,
+            std::size_t* charactersRead = nullptr) {
+        const std::size_t initial_position = pos;
 
-        // Branch will are encoded as [branchIdx, [branchValues...]].
+        // Branch will are encoded as one of the:
+        // [branchIdx, [branchValues...]]
+        // [branchIdx, branchValue]
+        // branchIdx
         RamDomain branchIdx = -1;
 
         auto&& adtInfo = types["ADTs"][adtName];
@@ -148,11 +149,12 @@
 
         // Consume initial character
         consumeChar(source, '$', pos);
-        std::string constructor = readAlphanumeric(source, pos);
+        std::string constructor = readIdentifier(source, pos);
 
         json11::Json branchInfo = [&]() -> json11::Json {
             for (auto branch : branches.array_items()) {
                 ++branchIdx;
+
                 if (branch["name"].string_value() == constructor) {
                     return branch;
                 }
@@ -169,19 +171,26 @@
             if (charactersRead != nullptr) {
                 *charactersRead = pos - initial_position;
             }
-            RamDomain emptyArgs = recordTable.pack(toVector<RamDomain>().data(), 0);
-            return recordTable.pack(toVector<RamDomain>(branchIdx, emptyArgs).data(), 2);
+
+            if (adtInfo["enum"].bool_value()) {
+                return branchIdx;
+            }
+
+            const RamDomain empty[] = {};
+            RamDomain emptyArgs = recordTable.pack(empty, 0);
+            const RamDomain record[] = {branchIdx, emptyArgs};
+            return recordTable.pack(record, 2);
         }
 
         consumeChar(source, '(', pos);
 
         std::vector<RamDomain> branchArgs(branchTypes.size());
 
-        for (size_t i = 0; i < branchTypes.size(); ++i) {
+        for (std::size_t i = 0; i < branchTypes.size(); ++i) {
             auto argType = branchTypes[i].string_value();
             assert(!argType.empty());
 
-            size_t consumed = 0;
+            std::size_t consumed = 0;
 
             if (i > 0) {
                 consumeChar(source, ',', pos);
@@ -190,7 +199,7 @@
 
             switch (argType[0]) {
                 case 's': {
-                    branchArgs[i] = symbolTable.unsafeLookup(readUntil(source, ",)", pos, &consumed));
+                    branchArgs[i] = symbolTable.encode(readSymbol(source, ",)", pos, &consumed));
                     break;
                 }
                 case 'i': {
@@ -233,31 +242,35 @@
             }
         }();
 
-        return recordTable.pack(toVector<RamDomain>(branchIdx, branchValue).data(), 2);
+        RamDomain rec[2] = {branchIdx, branchValue};
+        return recordTable.pack(rec, 2);
     }
 
     /**
-     * Read the next alphanumeric sequence (corresponding to IDENT).
+     * Read the next alphanumeric + ('_', '?') sequence (corresponding to IDENT).
      * Consume preceding whitespace.
      * TODO (darth_tytus): use std::string_view?
      */
-    std::string readAlphanumeric(const std::string& source, size_t& pos) {
+    std::string readIdentifier(const std::string& source, std::size_t& pos) {
         consumeWhiteSpace(source, pos);
         if (pos >= source.length()) {
             throw std::invalid_argument("Unexpected end of input");
         }
 
-        const size_t bgn = pos;
-        while (pos < source.length() && std::isalnum(static_cast<unsigned char>(source[pos]))) {
+        const std::size_t bgn = pos;
+        while (pos < source.length()) {
+            unsigned char ch = static_cast<unsigned char>(source[pos]);
+            bool valid = std::isalnum(ch) || ch == '_' || ch == '?';
+            if (!valid) break;
             ++pos;
         }
 
         return source.substr(bgn, pos - bgn);
     }
 
-    std::string readUntil(const std::string& source, const std::string stopChars, const size_t pos,
-            size_t* charactersRead) {
-        size_t endOfSymbol = source.find_first_of(stopChars, pos);
+    std::string readUntil(const std::string& source, const std::string& stopChars, const std::size_t pos,
+            std::size_t* charactersRead) {
+        std::size_t endOfSymbol = source.find_first_of(stopChars, pos);
 
         if (endOfSymbol == std::string::npos) {
             throw std::invalid_argument("Unexpected end of input");
@@ -268,10 +281,85 @@
         return source.substr(pos, *charactersRead);
     }
 
+    std::string readQuotedSymbol(const std::string& source, std::size_t pos, std::size_t* charactersRead) {
+        const std::size_t start = pos;
+        const std::size_t end = source.length();
+
+        const char quoteMark = source[pos];
+        ++pos;
+
+        const std::size_t startOfSymbol = pos;
+        std::size_t endOfSymbol = std::string::npos;
+        bool hasEscaped = false;
+
+        bool escaped = false;
+        while (pos < end) {
+            if (escaped) {
+                hasEscaped = true;
+                escaped = false;
+                ++pos;
+                continue;
+            }
+
+            const char c = source[pos];
+            if (c == quoteMark) {
+                endOfSymbol = pos;
+                ++pos;
+                break;
+            }
+            if (c == '\\') {
+                escaped = true;
+            }
+            ++pos;
+        }
+
+        if (endOfSymbol == std::string::npos) {
+            throw std::invalid_argument("Unexpected end of input");
+        }
+
+        *charactersRead = pos - start;
+
+        std::size_t lengthOfSymbol = endOfSymbol - startOfSymbol;
+
+        // fast handling of symbol without escape sequence
+        if (!hasEscaped) {
+            return source.substr(startOfSymbol, lengthOfSymbol);
+        } else {
+            // slow handling of symbol with escape sequence
+            std::string symbol;
+            symbol.reserve(lengthOfSymbol);
+            bool escaped = false;
+            for (std::size_t pos = startOfSymbol; pos < endOfSymbol; ++pos) {
+                char ch = source[pos];
+                if (escaped || ch != '\\') {
+                    symbol.push_back(ch);
+                    escaped = false;
+                } else {
+                    escaped = true;
+                }
+            }
+            return symbol;
+        }
+    }
+
     /**
+     * Read the next symbol.
+     * It is either a double-quoted symbol with backslash-escaped chars, or the
+     * longuest sequence that do not contains any of the given stopChars.
+     * */
+    std::string readSymbol(const std::string& source, const std::string& stopChars, const std::size_t pos,
+            std::size_t* charactersRead) {
+        if (source[pos] == '"') {
+            return readQuotedSymbol(source, pos, charactersRead);
+        } else {
+            return readUntil(source, stopChars, pos, charactersRead);
+        }
+    }
+
+    /**
      * Read past given character, consuming any preceding whitespace.
      */
-    void consumeChar(const std::string& str, char c, size_t& pos) {
+    void consumeChar(const std::string& str, char c, std::size_t& pos) {
         consumeWhiteSpace(str, pos);
         if (pos >= str.length()) {
             throw std::invalid_argument("Unexpected end of input");
@@ -287,7 +375,7 @@
     /**
      * Advance position in the string until first non-whitespace character.
      */
-    void consumeWhiteSpace(const std::string& str, size_t& pos) {
+    void consumeWhiteSpace(const std::string& str, std::size_t& pos) {
         while (pos < str.length() && std::isspace(static_cast<unsigned char>(str[pos]))) {
             ++pos;
         }
diff --git a/cbits/souffle/io/ReadStreamCSV.h b/cbits/souffle/io/ReadStreamCSV.h
--- a/cbits/souffle/io/ReadStreamCSV.h
+++ b/cbits/souffle/io/ReadStreamCSV.h
@@ -1,6 +1,6 @@
 /*
  * Souffle - A Datalog Compiler
- * Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved
+ * Copyright (c) 2021, The Souffle Developers. All rights reserved
  * Licensed under the Universal Permissive License v 1.0 as shown at:
  * - https://opensource.org/licenses/UPL
  * - <souffle root>/licenses/SOUFFLE-UPL.txt
@@ -15,6 +15,7 @@
 #pragma once
 
 #include "souffle/RamTypes.h"
+#include "souffle/RecordTable.h"
 #include "souffle/SymbolTable.h"
 #include "souffle/io/ReadStream.h"
 #include "souffle/utility/ContainerUtil.h"
@@ -40,15 +41,21 @@
 #include <vector>
 
 namespace souffle {
-class RecordTable;
 
 class ReadStreamCSV : public ReadStream {
 public:
     ReadStreamCSV(std::istream& file, const std::map<std::string, std::string>& rwOperation,
             SymbolTable& symbolTable, RecordTable& recordTable)
             : ReadStream(rwOperation, symbolTable, recordTable),
-              delimiter(getOr(rwOperation, "delimiter", "\t")), file(file), lineNumber(0),
+              rfc4180(getOr(rwOperation, "rfc4180", "false") == std::string("true")),
+              delimiter(getOr(rwOperation, "delimiter", (rfc4180 ? "," : "\t"))), file(file), lineNumber(0),
               inputMap(getInputColumnMap(rwOperation, static_cast<unsigned int>(arity))) {
+        if (rfc4180 && delimiter.find('"') != std::string::npos) {
+            std::stringstream errorMessage;
+            errorMessage << "CSV delimiter cannot contain '\"' character when rfc4180 is enabled.";
+            throw std::invalid_argument(errorMessage.str());
+        }
+
         while (inputMap.size() < arity) {
             int size = static_cast<int>(inputMap.size());
             inputMap[size] = size;
@@ -67,7 +74,7 @@
             return nullptr;
         }
         std::string line;
-        Own<RamDomain[]> tuple = std::make_unique<RamDomain[]>(typeAttributes.size());
+        Own<RamDomain[]> tuple = mk<RamDomain[]>(typeAttributes.size());
 
         if (!getline(file, line)) {
             return nullptr;
@@ -78,12 +85,11 @@
         }
         ++lineNumber;
 
-        size_t start = 0;
-        size_t end = 0;
-        size_t columnsFilled = 0;
+        std::size_t start = 0;
+        std::size_t columnsFilled = 0;
         for (uint32_t column = 0; columnsFilled < arity; column++) {
-            size_t charactersRead = 0;
-            std::string element = nextElement(line, start, end);
+            std::size_t charactersRead = 0;
+            std::string element = nextElement(line, start);
             if (inputMap.count(column) == 0) {
                 continue;
             }
@@ -93,7 +99,7 @@
                 auto&& ty = typeAttributes.at(inputMap[column]);
                 switch (ty[0]) {
                     case 's': {
-                        tuple[inputMap[column]] = symbolTable.unsafeLookup(element);
+                        tuple[inputMap[column]] = symbolTable.encode(element);
                         charactersRead = element.size();
                         break;
                     }
@@ -139,7 +145,7 @@
      * Read an unsigned element. Possible bases are 2, 10, 16
      * Base is indicated by the first two chars.
      */
-    RamUnsigned readRamUnsigned(const std::string& element, size_t& charactersRead) {
+    RamUnsigned readRamUnsigned(const std::string& element, std::size_t& charactersRead) {
         // Sanity check
         assert(element.size() > 0);
 
@@ -156,13 +162,64 @@
         return value;
     }
 
-    std::string nextElement(const std::string& line, size_t& start, size_t& end) {
+    std::string nextElement(const std::string& line, std::size_t& start) {
         std::string element;
 
+        if (rfc4180) {
+            if (line[start] == '"') {
+                // quoted field
+                const std::size_t end = line.length();
+                std::size_t pos = start + 1;
+                bool foundEndQuote = false;
+                while (pos < end) {
+                    char c = line[pos++];
+                    if (c == '"' && (pos < end) && line[pos] == '"') {
+                        // two double-quote => one double-quote
+                        element.push_back('"');
+                        ++pos;
+                    } else if (c == '"') {
+                        foundEndQuote = true;
+                        break;
+                    } else {
+                        element.push_back(c);
+                    }
+                }
+
+                if (!foundEndQuote) {
+                    // missing closing quote
+                    std::stringstream errorMessage;
+                    errorMessage << "Unbalanced field quote in line " << lineNumber << "; ";
+                    throw std::invalid_argument(errorMessage.str());
+                }
+
+                // field must be immediately followed by delimiter or end of line
+                if (pos != line.length()) {
+                    std::size_t nextDelimiter = line.find(delimiter, pos);
+                    if (nextDelimiter != pos) {
+                        std::stringstream errorMessage;
+                        errorMessage << "Separator expected immediately after quoted field in line "
+                                     << lineNumber << "; ";
+                        throw std::invalid_argument(errorMessage.str());
+                    }
+                }
+
+                start = pos + delimiter.size();
+                return element;
+            } else {
+                // non-quoted field, span until next delimiter or end of line
+                const std::size_t end = std::min(line.find(delimiter, start), line.length());
+                element = line.substr(start, end - start);
+                start = end + delimiter.size();
+
+                return element;
+            }
+        }
+
+        std::size_t end = start;
         // Handle record/tuple delimiter coincidence.
         if (delimiter.find(',') != std::string::npos) {
             int record_parens = 0;
-            size_t next_delimiter = line.find(delimiter, start);
+            std::size_t next_delimiter = line.find(delimiter, start);
 
             // Find first delimiter after the record.
             while (end < std::min(next_delimiter, line.length()) || record_parens != 0) {
@@ -190,7 +247,7 @@
             // Handle the end-of-the-line case where parenthesis are unbalanced.
             if (record_parens != 0) {
                 std::stringstream errorMessage;
-                errorMessage << "Unbalanced record parenthesis " << lineNumber << "; ";
+                errorMessage << "Unbalanced record parenthesis in line " << lineNumber << "; ";
                 throw std::invalid_argument(errorMessage.str());
             }
         } else {
@@ -234,9 +291,10 @@
         return inputColumnMap;
     }
 
+    const bool rfc4180;
     const std::string delimiter;
     std::istream& file;
-    size_t lineNumber;
+    std::size_t lineNumber;
     std::map<int, int> inputMap;
 };
 
@@ -248,7 +306,10 @@
               baseName(souffle::baseName(getFileName(rwOperation))),
               fileHandle(getFileName(rwOperation), std::ios::in | std::ios::binary) {
         if (!fileHandle.is_open()) {
-            throw std::invalid_argument("Cannot open fact file " + baseName + "\n");
+            // suppress error message in case file cannot be open when flag -w is set
+            if (getOr(rwOperation, "no-warn", "false") != "true") {
+                throw std::invalid_argument("Cannot open fact file " + baseName + "\n");
+            }
         }
         // Strip headers if we're using them
         if (getOr(rwOperation, "headers", "false") == "true") {
diff --git a/cbits/souffle/io/ReadStreamJSON.h b/cbits/souffle/io/ReadStreamJSON.h
--- a/cbits/souffle/io/ReadStreamJSON.h
+++ b/cbits/souffle/io/ReadStreamJSON.h
@@ -15,6 +15,7 @@
 #pragma once
 
 #include "souffle/RamTypes.h"
+#include "souffle/RecordTable.h"
 #include "souffle/SymbolTable.h"
 #include "souffle/io/ReadStream.h"
 #include "souffle/utility/ContainerUtil.h"
@@ -37,8 +38,14 @@
 #include <vector>
 
 namespace souffle {
-class RecordTable;
 
+template <typename... T>
+[[noreturn]] static void throwError(T const&... t) {
+    std::ostringstream out;
+    (out << ... << t);
+    throw std::runtime_error(out.str());
+}
+
 class ReadStreamJSON : public ReadStream {
 public:
     ReadStreamJSON(std::istream& file, const std::map<std::string, std::string>& rwOperation,
@@ -47,18 +54,18 @@
         std::string err;
         params = Json::parse(rwOperation.at("params"), err);
         if (err.length() > 0) {
-            fatal("cannot get internal params: %s", err);
+            throwError("cannot get internal params: ", err);
         }
     }
 
 protected:
     std::istream& file;
-    size_t pos;
+    std::size_t pos;
     Json jsonSource;
     Json params;
     bool isInitialized;
     bool useObjects;
-    std::map<const std::string, const size_t> paramIndex;
+    std::map<const std::string, const std::size_t> paramIndex;
 
     Own<RamDomain[]> readNextTuple() override {
         // for some reasons we cannot initalized our json objects in constructor
@@ -71,22 +78,27 @@
             jsonSource = Json::parse(source, error);
             // it should be wrapped by an extra array
             if (error.length() > 0 || !jsonSource.is_array()) {
-                fatal("cannot deserialize json because %s:\n%s", error, source);
+                throwError("cannot deserialize json because ", error, ":\n", source);
             }
 
+            if (jsonSource.array_items().empty()) {
+                // No tuples defined
+                return nullptr;
+            }
+
             // we only check the first one, since there are extra checks
             // in readNextTupleObject/readNextTupleList
             if (jsonSource[0].is_array()) {
                 useObjects = false;
             } else if (jsonSource[0].is_object()) {
                 useObjects = true;
-                size_t index_pos = 0;
+                std::size_t index_pos = 0;
                 for (auto param : params["relation"]["params"].array_items()) {
                     paramIndex.insert(std::make_pair(param.string_value(), index_pos));
                     index_pos++;
                 }
             } else {
-                fatal("the input is neither list nor object format");
+                throwError("the input is neither list nor object format");
             }
         }
 
@@ -102,16 +114,16 @@
             return nullptr;
         }
 
-        Own<RamDomain[]> tuple = std::make_unique<RamDomain[]>(typeAttributes.size());
+        Own<RamDomain[]> tuple = mk<RamDomain[]>(typeAttributes.size());
         const Json& jsonObj = jsonSource[pos];
         assert(jsonObj.is_array() && "the input is not json array");
         pos++;
-        for (size_t i = 0; i < typeAttributes.size(); ++i) {
+        for (std::size_t i = 0; i < typeAttributes.size(); ++i) {
             try {
                 auto&& ty = typeAttributes.at(i);
                 switch (ty[0]) {
                     case 's': {
-                        tuple[i] = symbolTable.unsafeLookup(jsonObj[i].string_value());
+                        tuple[i] = symbolTable.encode(jsonObj[i].string_value());
                         break;
                     }
                     case 'r': {
@@ -130,7 +142,7 @@
                         tuple[i] = static_cast<RamDomain>(jsonObj[i].number_value());
                         break;
                     }
-                    default: fatal("invalid type attribute: `%c`", ty[0]);
+                    default: throwError("invalid type attribute: '", ty[0], "'");
                 }
             } catch (...) {
                 std::stringstream errorMessage;
@@ -160,13 +172,13 @@
 
         assert(source.is_array() && "the input is not json array");
         auto&& recordTypes = recordInfo["types"];
-        const size_t recordArity = recordInfo["arity"].long_value();
+        const std::size_t recordArity = recordInfo["arity"].long_value();
         std::vector<RamDomain> recordValues(recordArity);
-        for (size_t i = 0; i < recordArity; ++i) {
+        for (std::size_t i = 0; i < recordArity; ++i) {
             const std::string& recordType = recordTypes[i].string_value();
             switch (recordType[0]) {
                 case 's': {
-                    recordValues[i] = symbolTable.unsafeLookup(source[i].string_value());
+                    recordValues[i] = symbolTable.encode(source[i].string_value());
                     break;
                 }
                 case 'r': {
@@ -185,7 +197,7 @@
                     recordValues[i] = static_cast<RamDomain>(source[i].number_value());
                     break;
                 }
-                default: fatal("invalid type attribute");
+                default: throwError("invalid type attribute");
             }
         }
 
@@ -197,7 +209,7 @@
             return nullptr;
         }
 
-        Own<RamDomain[]> tuple = std::make_unique<RamDomain[]>(typeAttributes.size());
+        Own<RamDomain[]> tuple = mk<RamDomain[]>(typeAttributes.size());
         const Json& jsonObj = jsonSource[pos];
         assert(jsonObj.is_object() && "the input is not json object");
         pos++;
@@ -205,13 +217,13 @@
             try {
                 // get the corresponding position by parameter name
                 if (paramIndex.find(p.first) == paramIndex.end()) {
-                    fatal("invalid parameter: %s", p.first);
+                    throwError("invalid parameter: ", p.first);
                 }
-                size_t i = paramIndex.at(p.first);
+                std::size_t i = paramIndex.at(p.first);
                 auto&& ty = typeAttributes.at(i);
                 switch (ty[0]) {
                     case 's': {
-                        tuple[i] = symbolTable.unsafeLookup(p.second.string_value());
+                        tuple[i] = symbolTable.encode(p.second.string_value());
                         break;
                     }
                     case 'r': {
@@ -230,7 +242,7 @@
                         tuple[i] = static_cast<RamDomain>(p.second.number_value());
                         break;
                     }
-                    default: fatal("invalid type attribute: `%c`", ty[0]);
+                    default: throwError("invalid type attribute: '", ty[0], "'");
                 }
             } catch (...) {
                 std::stringstream errorMessage;
@@ -245,9 +257,9 @@
     RamDomain readNextElementObject(const Json& source, const std::string& recordTypeName) {
         auto&& recordInfo = types["records"][recordTypeName];
         const std::string recordName = recordTypeName.substr(2);
-        std::map<const std::string, const size_t> recordIndex;
+        std::map<const std::string, const std::size_t> recordIndex;
 
-        size_t index_pos = 0;
+        std::size_t index_pos = 0;
         for (auto param : params["records"][recordName]["params"].array_items()) {
             recordIndex.insert(std::make_pair(param.string_value(), index_pos));
             index_pos++;
@@ -264,19 +276,19 @@
 
         assert(source.is_object() && "the input is not json object");
         auto&& recordTypes = recordInfo["types"];
-        const size_t recordArity = recordInfo["arity"].long_value();
+        const std::size_t recordArity = recordInfo["arity"].long_value();
         std::vector<RamDomain> recordValues(recordArity);
         recordValues.reserve(recordIndex.size());
         for (auto readParam : source.object_items()) {
             // get the corresponding position by parameter name
             if (recordIndex.find(readParam.first) == recordIndex.end()) {
-                fatal("invalid parameter: %s", readParam.first);
+                throwError("invalid parameter: ", readParam.first);
             }
-            size_t i = recordIndex.at(readParam.first);
+            std::size_t i = recordIndex.at(readParam.first);
             auto&& type = recordTypes[i].string_value();
             switch (type[0]) {
                 case 's': {
-                    recordValues[i] = symbolTable.unsafeLookup(readParam.second.string_value());
+                    recordValues[i] = symbolTable.encode(readParam.second.string_value());
                     break;
                 }
                 case 'r': {
@@ -295,7 +307,7 @@
                     recordValues[i] = static_cast<RamDomain>(readParam.second.number_value());
                     break;
                 }
-                default: fatal("invalid type attribute: `%c`", type[0]);
+                default: throwError("invalid type attribute: '", type[0], "'");
             }
         }
 
@@ -307,6 +319,8 @@
 public:
     ReadFileJSON(const std::map<std::string, std::string>& rwOperation, SymbolTable& symbolTable,
             RecordTable& recordTable)
+            // FIXME: This is bordering on UB - we're passing an unconstructed
+            // object (fileHandle) to the base class
             : ReadStreamJSON(fileHandle, rwOperation, symbolTable, recordTable),
               baseName(souffle::baseName(getFileName(rwOperation))),
               fileHandle(getFileName(rwOperation), std::ios::in | std::ios::binary) {
diff --git a/cbits/souffle/io/ReadStreamSQLite.h b/cbits/souffle/io/ReadStreamSQLite.h
--- a/cbits/souffle/io/ReadStreamSQLite.h
+++ b/cbits/souffle/io/ReadStreamSQLite.h
@@ -1,6 +1,6 @@
 /*
  * Souffle - A Datalog Compiler
- * Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved
+ * Copyright (c) 2021, The Souffle Developers. All rights reserved
  * Licensed under the Universal Permissive License v 1.0 as shown at:
  * - https://opensource.org/licenses/UPL
  * - <souffle root>/licenses/SOUFFLE-UPL.txt
@@ -15,6 +15,7 @@
 #pragma once
 
 #include "souffle/RamTypes.h"
+#include "souffle/RecordTable.h"
 #include "souffle/SymbolTable.h"
 #include "souffle/io/ReadStream.h"
 #include "souffle/utility/MiscUtil.h"
@@ -30,7 +31,6 @@
 #include <sqlite3.h>
 
 namespace souffle {
-class RecordTable;
 
 class ReadStreamSQLite : public ReadStream {
 public:
@@ -60,7 +60,7 @@
             return nullptr;
         }
 
-        Own<RamDomain[]> tuple = std::make_unique<RamDomain[]>(arity + auxiliaryArity);
+        Own<RamDomain[]> tuple = mk<RamDomain[]>(arity + auxiliaryArity);
 
         uint32_t column;
         for (column = 0; column < arity; column++) {
@@ -73,7 +73,7 @@
             try {
                 auto&& ty = typeAttributes.at(column);
                 switch (ty[0]) {
-                    case 's': tuple[column] = symbolTable.unsafeLookup(element); break;
+                    case 's': tuple[column] = symbolTable.encode(element); break;
                     case 'i':
                     case 'u':
                     case 'f':
diff --git a/cbits/souffle/io/SerialisationStream.h b/cbits/souffle/io/SerialisationStream.h
--- a/cbits/souffle/io/SerialisationStream.h
+++ b/cbits/souffle/io/SerialisationStream.h
@@ -18,6 +18,7 @@
 
 #include "souffle/RamTypes.h"
 
+#include "souffle/utility/StringUtil.h"
 #include "souffle/utility/json11.h"
 #include <cassert>
 #include <cstddef>
@@ -28,7 +29,7 @@
 
 namespace souffle {
 
-class RecordTable;
+class RecordTableInterface;
 class SymbolTable;
 
 using json11::Json;
@@ -42,48 +43,54 @@
     template <typename A>
     using RO = std::conditional_t<readOnlyTables, const A, A>;
 
-    SerialisationStream(RO<SymbolTable>& symTab, RO<RecordTable>& recTab, Json types,
-            std::vector<std::string> relTypes, size_t auxArity = 0)
+    SerialisationStream(RO<SymbolTable>& symTab, RO<RecordTableInterface>& recTab, Json types,
+            std::vector<std::string> relTypes, std::size_t auxArity = 0)
             : symbolTable(symTab), recordTable(recTab), types(std::move(types)),
               typeAttributes(std::move(relTypes)), arity(typeAttributes.size() - auxArity),
               auxiliaryArity(auxArity) {}
 
-    SerialisationStream(RO<SymbolTable>& symTab, RO<RecordTable>& recTab, Json types)
+    SerialisationStream(RO<SymbolTable>& symTab, RO<RecordTableInterface>& recTab, Json types)
             : symbolTable(symTab), recordTable(recTab), types(std::move(types)) {
         setupFromJson();
     }
 
-    SerialisationStream(RO<SymbolTable>& symTab, RO<RecordTable>& recTab,
+    SerialisationStream(RO<SymbolTable>& symTab, RO<RecordTableInterface>& recTab,
             const std::map<std::string, std::string>& rwOperation)
             : symbolTable(symTab), recordTable(recTab) {
         std::string parseErrors;
         types = Json::parse(rwOperation.at("types"), parseErrors);
         assert(parseErrors.size() == 0 && "Internal JSON parsing failed.");
+
+        auxiliaryArity = RamSignedFromString(getOr(rwOperation, "auxArity", "0"));
+
         setupFromJson();
     }
 
     RO<SymbolTable>& symbolTable;
-    RO<RecordTable>& recordTable;
+    RO<RecordTableInterface>& recordTable;
     Json types;
     std::vector<std::string> typeAttributes;
 
-    size_t arity = 0;
-    size_t auxiliaryArity = 0;
+    std::size_t arity = 0;
+    std::size_t auxiliaryArity = 0;
 
 private:
     void setupFromJson() {
         auto&& relInfo = types["relation"];
-        arity = static_cast<size_t>(relInfo["arity"].long_value());
-        auxiliaryArity = static_cast<size_t>(relInfo["auxArity"].long_value());
+        arity = static_cast<std::size_t>(relInfo["arity"].long_value());
 
         assert(relInfo["types"].is_array());
         auto&& relTypes = relInfo["types"].array_items();
-        assert(relTypes.size() == (arity + auxiliaryArity));
+        assert(relTypes.size() == arity);
 
-        for (size_t i = 0; i < arity + auxiliaryArity; ++i) {
-            auto&& type = relTypes[i].string_value();
-            assert(!type.empty() && "malformed types tag");
-            typeAttributes.push_back(type);
+        for (const auto& jsonType : relTypes) {
+            const auto& typeString = jsonType.string_value();
+            assert(!typeString.empty() && "malformed types tag");
+            typeAttributes.push_back(typeString);
+        }
+
+        for (std::size_t i = 0; i < auxiliaryArity; i++) {
+            typeAttributes.push_back("i:number");
         }
     }
 };
diff --git a/cbits/souffle/io/WriteStream.h b/cbits/souffle/io/WriteStream.h
--- a/cbits/souffle/io/WriteStream.h
+++ b/cbits/souffle/io/WriteStream.h
@@ -1,6 +1,6 @@
 /*
  * Souffle - A Datalog Compiler
- * Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved
+ * Copyright (c) 2021, The Souffle Developers. All rights reserved
  * Licensed under the Universal Permissive License v 1.0 as shown at:
  * - https://opensource.org/licenses/UPL
  * - <souffle root>/licenses/SOUFFLE-UPL.txt
@@ -22,6 +22,7 @@
 #include "souffle/utility/json11.h"
 #include <cassert>
 #include <cstddef>
+#include <iomanip>
 #include <map>
 #include <memory>
 #include <ostream>
@@ -43,8 +44,6 @@
         if (summary) {
             return writeSize(relation.size());
         }
-        auto lease = symbolTable.acquireLock();
-        (void)lease;  // silence "unused variable" warning
         if (arity == 0) {
             if (relation.begin() != relation.end()) {
                 writeNullary();
@@ -72,9 +71,14 @@
 
     template <typename Tuple>
     void writeNext(const Tuple tuple) {
-        writeNextTuple(tuple.data);
+        using tcb::make_span;
+        writeNextTuple(make_span(tuple).data());
     }
 
+    virtual void outputSymbol(std::ostream& destination, const std::string& value) {
+        destination << value;
+    }
+
     void outputRecord(std::ostream& destination, const RamDomain value, const std::string& name) {
         auto&& recordInfo = types["records"][name];
 
@@ -88,14 +92,14 @@
         }
 
         auto&& recordTypes = recordInfo["types"];
-        const size_t recordArity = recordInfo["arity"].long_value();
+        const std::size_t recordArity = recordInfo["arity"].long_value();
 
         const RamDomain* tuplePtr = recordTable.unpack(value, recordArity);
 
         destination << "[";
 
         // print record's elements
-        for (size_t i = 0; i < recordArity; ++i) {
+        for (std::size_t i = 0; i < recordArity; ++i) {
             if (i > 0) {
                 destination << ", ";
             }
@@ -107,7 +111,7 @@
                 case 'i': destination << recordValue; break;
                 case 'f': destination << ramBitCast<RamFloat>(recordValue); break;
                 case 'u': destination << ramBitCast<RamUnsigned>(recordValue); break;
-                case 's': destination << symbolTable.unsafeResolve(recordValue); break;
+                case 's': outputSymbol(destination, symbolTable.decode(recordValue)); break;
                 case 'r': outputRecord(destination, recordValue, recordType); break;
                 case '+': outputADT(destination, recordValue, recordType); break;
                 default: fatal("Unsupported type attribute: `%c`", recordType[0]);
@@ -121,28 +125,40 @@
 
         assert(!adtInfo.is_null() && "Missing adt type information");
 
-        const size_t numBranches = adtInfo["arity"].long_value();
+        const std::size_t numBranches = adtInfo["arity"].long_value();
         assert(numBranches > 0);
 
-        // adt is encoded as [branchID, [branch_args]] when |branch_args| != 1
-        // and as [branchID, arg] when a branch takes a single argument.
-        const RamDomain* tuplePtr = recordTable.unpack(value, 2);
+        // adt is encoded in one of three possible ways:
+        // [branchID, [branch_args]] when |branch_args| != 1
+        // [branchID, arg] when a branch takes a single argument.
+        // branchID when ADT is an enumeration.
+        bool isEnum = adtInfo["enum"].bool_value();
 
-        const RamDomain branchId = tuplePtr[0];
-        const RamDomain rawBranchArgs = tuplePtr[1];
+        RamDomain branchId = value;
+        const RamDomain* branchArgs = nullptr;
+        json11::Json branchInfo;
+        json11::Json::array branchTypes;
 
-        auto branchInfo = adtInfo["branches"][branchId];
-        auto branchTypes = branchInfo["types"].array_items();
+        if (!isEnum) {
+            const RamDomain* tuplePtr = recordTable.unpack(value, 2);
 
-        // Prepare branch's arguments for output.
-        const RamDomain* branchArgs = [&]() -> const RamDomain* {
-            if (branchTypes.size() > 1) {
-                return recordTable.unpack(rawBranchArgs, branchTypes.size());
-            } else {
-                return &rawBranchArgs;
-            }
-        }();
+            branchId = tuplePtr[0];
+            branchInfo = adtInfo["branches"][branchId];
+            branchTypes = branchInfo["types"].array_items();
 
+            // Prepare branch's arguments for output.
+            branchArgs = [&]() -> const RamDomain* {
+                if (branchTypes.size() > 1) {
+                    return recordTable.unpack(tuplePtr[1], branchTypes.size());
+                } else {
+                    return &tuplePtr[1];
+                }
+            }();
+        } else {
+            branchInfo = adtInfo["branches"][branchId];
+            branchTypes = branchInfo["types"].array_items();
+        }
+
         destination << "$" << branchInfo["name"].string_value();
 
         if (branchTypes.size() > 0) {
@@ -150,7 +166,7 @@
         }
 
         // Print arguments
-        for (size_t i = 0; i < branchTypes.size(); ++i) {
+        for (std::size_t i = 0; i < branchTypes.size(); ++i) {
             if (i > 0) {
                 destination << ", ";
             }
@@ -160,7 +176,7 @@
                 case 'i': destination << branchArgs[i]; break;
                 case 'f': destination << ramBitCast<RamFloat>(branchArgs[i]); break;
                 case 'u': destination << ramBitCast<RamUnsigned>(branchArgs[i]); break;
-                case 's': destination << symbolTable.unsafeResolve(branchArgs[i]); break;
+                case 's': outputSymbol(destination, symbolTable.decode(branchArgs[i])); break;
                 case 'r': outputRecord(destination, branchArgs[i], argType); break;
                 case '+': outputADT(destination, branchArgs[i], argType); break;
                 default: fatal("Unsupported type attribute: `%c`", argType[0]);
diff --git a/cbits/souffle/io/WriteStreamCSV.h b/cbits/souffle/io/WriteStreamCSV.h
--- a/cbits/souffle/io/WriteStreamCSV.h
+++ b/cbits/souffle/io/WriteStreamCSV.h
@@ -1,6 +1,6 @@
 /*
  * Souffle - A Datalog Compiler
- * Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved
+ * Copyright (c) 2021, The Souffle Developers. All rights reserved
  * Licensed under the Universal Permissive License v 1.0 as shown at:
  * - https://opensource.org/licenses/UPL
  * - <souffle root>/licenses/SOUFFLE-UPL.txt
@@ -15,6 +15,7 @@
 #pragma once
 
 #include "souffle/RamTypes.h"
+#include "souffle/RecordTable.h"
 #include "souffle/SymbolTable.h"
 #include "souffle/io/WriteStream.h"
 #include "souffle/utility/ContainerUtil.h"
@@ -35,21 +36,28 @@
 
 namespace souffle {
 
-class RecordTable;
-
 class WriteStreamCSV : public WriteStream {
 protected:
     WriteStreamCSV(const std::map<std::string, std::string>& rwOperation, const SymbolTable& symbolTable,
             const RecordTable& recordTable)
             : WriteStream(rwOperation, symbolTable, recordTable),
-              delimiter(getOr(rwOperation, "delimiter", "\t")){};
+              rfc4180(getOr(rwOperation, "rfc4180", "false") == std::string("true")),
+              delimiter(getOr(rwOperation, "delimiter", (rfc4180 ? "," : "\t"))) {
+        if (rfc4180 && delimiter.find('"') != std::string::npos) {
+            std::stringstream errorMessage;
+            errorMessage << "CSV delimiter cannot contain '\"' character when rfc4180 is enabled.";
+            throw std::invalid_argument(errorMessage.str());
+        }
+    };
 
+    const bool rfc4180;
+
     const std::string delimiter;
 
     void writeNextTupleCSV(std::ostream& destination, const RamDomain* tuple) {
         writeNextTupleElement(destination, typeAttributes.at(0), tuple[0]);
 
-        for (size_t col = 1; col < arity; ++col) {
+        for (std::size_t col = 1; col < arity; ++col) {
             destination << delimiter;
             writeNextTupleElement(destination, typeAttributes.at(col), tuple[col]);
         }
@@ -57,14 +65,60 @@
         destination << "\n";
     }
 
+    virtual void outputSymbol(std::ostream& destination, const std::string& value) {
+        outputSymbol(destination, value, false);
+    }
+
+    void outputSymbol(std::ostream& destination, const std::string& value, bool fieldValue) {
+        if (rfc4180) {
+            if (!fieldValue) {
+                destination << '"';
+            }
+            destination << '"';
+
+            const std::size_t end = value.length();
+            for (std::size_t pos = 0; pos < end; ++pos) {
+                char ch = value[pos];
+                if (ch == '"') {
+                    destination << '\\';
+                    destination << '"';
+                }
+                destination << ch;
+            }
+
+            if (!fieldValue) {
+                destination << '"';
+            }
+            destination << '"';
+        } else {
+            destination << value;
+        }
+    }
+
     void writeNextTupleElement(std::ostream& destination, const std::string& type, RamDomain value) {
         switch (type[0]) {
-            case 's': destination << symbolTable.unsafeResolve(value); break;
+            case 's': outputSymbol(destination, symbolTable.decode(value), true); break;
             case 'i': destination << value; break;
             case 'u': destination << ramBitCast<RamUnsigned>(value); break;
             case 'f': destination << ramBitCast<RamFloat>(value); break;
-            case 'r': outputRecord(destination, value, type); break;
-            case '+': outputADT(destination, value, type); break;
+            case 'r':
+                if (rfc4180) {
+                    destination << '"';
+                }
+                outputRecord(destination, value, type);
+                if (rfc4180) {
+                    destination << '"';
+                }
+                break;
+            case '+':
+                if (rfc4180) {
+                    destination << '"';
+                }
+                outputADT(destination, value, type);
+                if (rfc4180) {
+                    destination << '"';
+                }
+                break;
             default: fatal("unsupported type attribute: `%c`", type[0]);
         }
     }
@@ -183,8 +237,9 @@
 
 class WriteCoutPrintSize : public WriteStream {
 public:
-    explicit WriteCoutPrintSize(const std::map<std::string, std::string>& rwOperation)
-            : WriteStream(rwOperation, {}, {}), lease(souffle::getOutputLock().acquire()) {
+    WriteCoutPrintSize(const std::map<std::string, std::string>& rwOperation, const SymbolTable& symbolTable,
+            const RecordTable& recordTable)
+            : WriteStream(rwOperation, symbolTable, recordTable), lease(souffle::getOutputLock().acquire()) {
         std::cout << rwOperation.at("name") << "\t";
     }
 
@@ -240,9 +295,9 @@
 
 class WriteCoutPrintSizeFactory : public WriteStreamFactory {
 public:
-    Own<WriteStream> getWriter(const std::map<std::string, std::string>& rwOperation, const SymbolTable&,
-            const RecordTable&) override {
-        return mk<WriteCoutPrintSize>(rwOperation);
+    Own<WriteStream> getWriter(const std::map<std::string, std::string>& rwOperation,
+            const SymbolTable& symbolTable, const RecordTable& recordTable) override {
+        return mk<WriteCoutPrintSize>(rwOperation, symbolTable, recordTable);
     }
     const std::string& getName() const override {
         static const std::string name = "stdoutprintsize";
diff --git a/cbits/souffle/io/WriteStreamJSON.h b/cbits/souffle/io/WriteStreamJSON.h
--- a/cbits/souffle/io/WriteStreamJSON.h
+++ b/cbits/souffle/io/WriteStreamJSON.h
@@ -56,7 +56,7 @@
         else
             destination << "[";
 
-        for (size_t col = 0; col < arity; ++col) {
+        for (std::size_t col = 0; col < arity; ++col) {
             if (col > 0) {
                 destination << ", ";
             }
@@ -96,7 +96,7 @@
             assert(currType.length() > 2 && "Invalid type length");
             switch (currType[0]) {
                 // since some strings may need to be escaped, we use dump here
-                case 's': destination << Json(symbolTable.unsafeResolve(currValue)).dump(); break;
+                case 's': destination << Json(symbolTable.decode(currValue)).dump(); break;
                 case 'i': destination << currValue; break;
                 case 'u': destination << (int)ramBitCast<RamUnsigned>(currValue); break;
                 case 'f': destination << ramBitCast<RamFloat>(currValue); break;
@@ -109,7 +109,7 @@
                     }
 
                     auto&& recordTypes = recordInfo["types"];
-                    const size_t recordArity = recordInfo["arity"].long_value();
+                    const std::size_t recordArity = recordInfo["arity"].long_value();
                     const RamDomain* tuplePtr = recordTable.unpack(currValue, recordArity);
                     worklist.push("]");
                     for (auto i = (long long)(recordArity - 1); i >= 0; --i) {
@@ -151,7 +151,7 @@
             assert(currType.length() > 2 && "Invalid type length");
             switch (currType[0]) {
                 // since some strings may need to be escaped, we use dump here
-                case 's': destination << Json(symbolTable.unsafeResolve(currValue)).dump(); break;
+                case 's': destination << Json(symbolTable.decode(currValue)).dump(); break;
                 case 'i': destination << currValue; break;
                 case 'u': destination << (int)ramBitCast<RamUnsigned>(currValue); break;
                 case 'f': destination << ramBitCast<RamFloat>(currValue); break;
@@ -164,7 +164,7 @@
                     }
 
                     auto&& recordTypes = recordInfo["types"];
-                    const size_t recordArity = recordInfo["arity"].long_value();
+                    const std::size_t recordArity = recordInfo["arity"].long_value();
                     const RamDomain* tuplePtr = recordTable.unpack(currValue, recordArity);
                     worklist.push("}");
                     for (auto i = (long long)(recordArity - 1); i >= 0; --i) {
diff --git a/cbits/souffle/io/WriteStreamSQLite.h b/cbits/souffle/io/WriteStreamSQLite.h
--- a/cbits/souffle/io/WriteStreamSQLite.h
+++ b/cbits/souffle/io/WriteStreamSQLite.h
@@ -1,6 +1,6 @@
 /*
  * Souffle - A Datalog Compiler
- * Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved
+ * Copyright (c) 2021, The Souffle Developers. All rights reserved
  * Licensed under the Universal Permissive License v 1.0 as shown at:
  * - https://opensource.org/licenses/UPL
  * - <souffle root>/licenses/SOUFFLE-UPL.txt
@@ -15,6 +15,7 @@
 #pragma once
 
 #include "souffle/RamTypes.h"
+#include "souffle/RecordTable.h"
 #include "souffle/SymbolTable.h"
 #include "souffle/io/WriteStream.h"
 #include <cassert>
@@ -31,8 +32,6 @@
 
 namespace souffle {
 
-class RecordTable;
-
 class WriteStreamSQLite : public WriteStream {
 public:
     WriteStreamSQLite(const std::map<std::string, std::string>& rwOperation, const SymbolTable& symbolTable,
@@ -42,10 +41,11 @@
         openDB();
         createTables();
         prepareStatements();
-        //        executeSQL("BEGIN TRANSACTION", db);
+        executeSQL("BEGIN TRANSACTION", db);
     }
 
     ~WriteStreamSQLite() override {
+        executeSQL("COMMIT", db);
         sqlite3_finalize(insertStatement);
         sqlite3_finalize(symbolInsertStatement);
         sqlite3_finalize(symbolSelectStatement);
@@ -56,7 +56,7 @@
     void writeNullary() override {}
 
     void writeNextTuple(const RamDomain* tuple) override {
-        for (size_t i = 0; i < arity; i++) {
+        for (std::size_t i = 0; i < arity; i++) {
             RamDomain value = 0;  // Silence warning
 
             switch (typeAttributes.at(i)[0]) {
@@ -103,7 +103,7 @@
     }
 
     uint64_t getSymbolTableIDFromDB(int index) {
-        if (sqlite3_bind_text(symbolSelectStatement, 1, symbolTable.unsafeResolve(index).c_str(), -1,
+        if (sqlite3_bind_text(symbolSelectStatement, 1, symbolTable.decode(index).c_str(), -1,
                     SQLITE_TRANSIENT) != SQLITE_OK) {
             throwError("SQLite error in sqlite3_bind_text: ");
         }
@@ -120,7 +120,7 @@
             return dbSymbolTable[index];
         }
 
-        if (sqlite3_bind_text(symbolInsertStatement, 1, symbolTable.unsafeResolve(index).c_str(), -1,
+        if (sqlite3_bind_text(symbolInsertStatement, 1, symbolTable.decode(index).c_str(), -1,
                     SQLITE_TRANSIENT) != SQLITE_OK) {
             throwError("SQLite error in sqlite3_bind_text: ");
         }
diff --git a/cbits/souffle/io/gzfstream.h b/cbits/souffle/io/gzfstream.h
--- a/cbits/souffle/io/gzfstream.h
+++ b/cbits/souffle/io/gzfstream.h
@@ -1,6 +1,6 @@
 /*
  * Souffle - A Datalog Compiler
- * Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved
+ * Copyright (c) 2021, The Souffle Developers. All rights reserved
  * Licensed under the Universal Permissive License v 1.0 as shown at:
  * - https://opensource.org/licenses/UPL
  * - <souffle root>/licenses/SOUFFLE-UPL.txt
diff --git a/cbits/souffle/utility/CacheUtil.h b/cbits/souffle/utility/CacheUtil.h
--- a/cbits/souffle/utility/CacheUtil.h
+++ b/cbits/souffle/utility/CacheUtil.h
@@ -1,6 +1,6 @@
 /*
  * Souffle - A Datalog Compiler
- * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved
+ * Copyright (c) 2021, The Souffle Developers. All rights reserved
  * Licensed under the Universal Permissive License v 1.0 as shown at:
  * - https://opensource.org/licenses/UPL
  * - <souffle root>/licenses/SOUFFLE-UPL.txt
diff --git a/cbits/souffle/utility/ContainerUtil.h b/cbits/souffle/utility/ContainerUtil.h
--- a/cbits/souffle/utility/ContainerUtil.h
+++ b/cbits/souffle/utility/ContainerUtil.h
@@ -1,6 +1,6 @@
 /*
  * Souffle - A Datalog Compiler
- * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved
+ * Copyright (c) 2021, The Souffle Developers. All rights reserved
  * Licensed under the Universal Permissive License v 1.0 as shown at:
  * - https://opensource.org/licenses/UPL
  * - <souffle root>/licenses/SOUFFLE-UPL.txt
@@ -16,11 +16,13 @@
 
 #pragma once
 
+#include "souffle/utility/Iteration.h"
+#include "souffle/utility/MiscUtil.h"
+
 #include <algorithm>
 #include <functional>
 #include <iterator>
 #include <map>
-#include <memory>
 #include <set>
 #include <type_traits>
 #include <utility>
@@ -32,17 +34,6 @@
 //                           General Container Utilities
 // -------------------------------------------------------------------------------
 
-template <typename A>
-using Own = std::unique_ptr<A>;
-
-template <typename A>
-using VecOwn = std::vector<Own<A>>;
-
-template <typename A, typename B = A, typename... Args>
-Own<A> mk(Args&&... xs) {
-    return Own<A>(new B(std::forward<Args>(xs)...));
-}
-
 /**
  * Use to range-for iterate in reverse.
  * Assumes `std::rbegin` and `std::rend` are defined for type `A`.
@@ -113,7 +104,28 @@
     }
 }
 
+namespace detail {
+inline auto allOfBool = [](bool b) { return b; };
+}
+
 /**
+ * Return true if all elements (optionally after applying up)
+ * are true
+ */
+template <typename R, typename UnaryP = decltype(detail::allOfBool) const&>
+bool all(R const& range, UnaryP&& up = detail::allOfBool) {
+    return std::all_of(range.begin(), range.end(), std::forward<UnaryP>(up));
+}
+
+/**
+ * Append elements to a container
+ */
+template <class C, typename R>
+void append(C& container, R&& range) {
+    container.insert(container.end(), std::begin(range), std::end(range));
+}
+
+/**
  * A utility function enabling the creation of a vector with a fixed set of
  * elements within a single expression. This is the base case covering empty
  * vectors.
@@ -137,7 +149,7 @@
  * A utility function enabling the creation of a vector of pointers.
  */
 template <typename T>
-std::vector<T*> toPtrVector(const std::vector<std::unique_ptr<T>>& v) {
+std::vector<T*> toPtrVector(const VecOwn<T>& v) {
     std::vector<T*> res;
     for (auto& e : v) {
         res.push_back(e.get());
@@ -150,6 +162,8 @@
  */
 template <typename A, typename F /* : A -> B */>
 auto map(const std::vector<A>& xs, F&& f) {
+    // FIXME: We can rewrite this using makeTransformRange now,
+    // or remove the usage of this completely
     std::vector<decltype(f(xs[0]))> ys;
     ys.reserve(xs.size());
     for (auto&& x : xs) {
@@ -159,232 +173,6 @@
 }
 
 // -------------------------------------------------------------------------------
-//                             Cloning Utilities
-// -------------------------------------------------------------------------------
-
-template <typename A>
-auto clone(const std::vector<A*>& xs) {
-    std::vector<std::unique_ptr<A>> ys;
-    ys.reserve(xs.size());
-    for (auto&& x : xs) {
-        ys.emplace_back(x ? std::unique_ptr<A>(x->clone()) : nullptr);
-    }
-    return ys;
-}
-
-template <typename A>
-auto clone(const std::vector<std::unique_ptr<A>>& xs) {
-    std::vector<std::unique_ptr<A>> ys;
-    ys.reserve(xs.size());
-    for (auto&& x : xs) {
-        ys.emplace_back(x ? std::unique_ptr<A>(x->clone()) : nullptr);
-    }
-    return ys;
-}
-
-// -------------------------------------------------------------
-//                            Iterators
-// -------------------------------------------------------------
-
-/**
- * A wrapper for an iterator obtaining pointers of a certain type,
- * dereferencing values before forwarding them to the consumer.
- *
- * @tparam Iter ... the type of wrapped iterator
- * @tparam T    ... the value to be accessed by the resulting iterator
- */
-template <typename Iter, typename T = typename std::remove_pointer<typename Iter::value_type>::type>
-struct IterDerefWrapper : public std::iterator<std::forward_iterator_tag, T> {
-    /* The nested iterator. */
-    Iter iter;
-
-public:
-    // some constructors
-    IterDerefWrapper() = default;
-    IterDerefWrapper(const Iter& iter) : iter(iter) {}
-
-    // defaulted copy and move constructors
-    IterDerefWrapper(const IterDerefWrapper&) = default;
-    IterDerefWrapper(IterDerefWrapper&&) = default;
-
-    // default assignment operators
-    IterDerefWrapper& operator=(const IterDerefWrapper&) = default;
-    IterDerefWrapper& operator=(IterDerefWrapper&&) = default;
-
-    /* The equality operator as required by the iterator concept. */
-    bool operator==(const IterDerefWrapper& other) const {
-        return iter == other.iter;
-    }
-
-    /* The not-equality operator as required by the iterator concept. */
-    bool operator!=(const IterDerefWrapper& other) const {
-        return iter != other.iter;
-    }
-
-    /* The deref operator as required by the iterator concept. */
-    const T& operator*() const {
-        return **iter;
-    }
-
-    /* Support for the pointer operator. */
-    const T* operator->() const {
-        return &(**iter);
-    }
-
-    /* The increment operator as required by the iterator concept. */
-    IterDerefWrapper& operator++() {
-        ++iter;
-        return *this;
-    }
-};
-
-/**
- * A factory function enabling the construction of a dereferencing
- * iterator utilizing the automated deduction of template parameters.
- */
-template <typename Iter>
-IterDerefWrapper<Iter> derefIter(const Iter& iter) {
-    return IterDerefWrapper<Iter>(iter);
-}
-
-/**
- * An iterator to be utilized if there is only a single element to iterate over.
- */
-template <typename T>
-class SingleValueIterator : public std::iterator<std::forward_iterator_tag, T> {
-    T value;
-
-    bool end = true;
-
-public:
-    SingleValueIterator() = default;
-
-    SingleValueIterator(const T& value) : value(value), end(false) {}
-
-    // a copy constructor
-    SingleValueIterator(const SingleValueIterator& other) = default;
-
-    // an assignment operator
-    SingleValueIterator& operator=(const SingleValueIterator& other) = default;
-
-    // the equality operator as required by the iterator concept
-    bool operator==(const SingleValueIterator& other) const {
-        // only equivalent if pointing to the end
-        return end && other.end;
-    }
-
-    // the not-equality operator as required by the iterator concept
-    bool operator!=(const SingleValueIterator& other) const {
-        return !(*this == other);
-    }
-
-    // the deref operator as required by the iterator concept
-    const T& operator*() const {
-        return value;
-    }
-
-    // support for the pointer operator
-    const T* operator->() const {
-        return &value;
-    }
-
-    // the increment operator as required by the iterator concept
-    SingleValueIterator& operator++() {
-        end = true;
-        return *this;
-    }
-};
-
-// -------------------------------------------------------------
-//                             Ranges
-// -------------------------------------------------------------
-
-/**
- * A utility class enabling representation of ranges by pairing
- * two iterator instances marking lower and upper boundaries.
- */
-template <typename Iter>
-struct range {
-    // the lower and upper boundary
-    Iter a, b;
-
-    // a constructor accepting a lower and upper boundary
-    range(Iter a, Iter b) : a(std::move(a)), b(std::move(b)) {}
-
-    // default copy / move and assignment support
-    range(const range&) = default;
-    range(range&&) = default;
-    range& operator=(const range&) = default;
-
-    // get the lower boundary (for for-all loop)
-    Iter& begin() {
-        return a;
-    }
-    const Iter& begin() const {
-        return a;
-    }
-
-    // get the upper boundary (for for-all loop)
-    Iter& end() {
-        return b;
-    }
-    const Iter& end() const {
-        return b;
-    }
-
-    // emptiness check
-    bool empty() const {
-        return a == b;
-    }
-
-    // splits up this range into the given number of partitions
-    std::vector<range> partition(int np = 100) {
-        // obtain the size
-        int n = 0;
-        for (auto i = a; i != b; ++i) {
-            n++;
-        }
-
-        // split it up
-        auto s = n / np;
-        auto r = n % np;
-        std::vector<range> res;
-        res.reserve(np);
-        auto cur = a;
-        auto last = cur;
-        int i = 0;
-        int p = 0;
-        while (cur != b) {
-            ++cur;
-            i++;
-            if (i >= (s + (p < r ? 1 : 0))) {
-                res.push_back({last, cur});
-                last = cur;
-                p++;
-                i = 0;
-            }
-        }
-        if (cur != last) {
-            res.push_back({last, cur});
-        }
-        return res;
-    }
-};
-
-/**
- * A utility function enabling the construction of ranges
- * without explicitly specifying the iterator type.
- *
- * @tparam Iter .. the iterator type
- * @param a .. the lower boundary
- * @param b .. the upper boundary
- */
-template <typename Iter>
-range<Iter> make_range(const Iter& a, const Iter& b) {
-    return range<Iter>(a, b);
-}
-
-// -------------------------------------------------------------------------------
 //                             Equality Utilities
 // -------------------------------------------------------------------------------
 
@@ -396,8 +184,8 @@
  */
 template <typename toType, typename baseType>
 bool castEq(const baseType* left, const baseType* right) {
-    if (auto castedLeft = dynamic_cast<const toType*>(left)) {
-        if (auto castedRight = dynamic_cast<const toType*>(right)) {
+    if (auto castedLeft = as<toType>(left)) {
+        if (auto castedRight = as<toType>(right)) {
             return castedLeft == castedRight;
         }
     }
@@ -453,8 +241,8 @@
  * targets.
  */
 template <typename T, template <typename...> class Container>
-bool equal_targets(const Container<std::unique_ptr<T>>& a, const Container<std::unique_ptr<T>>& b) {
-    return equal_targets(a, b, comp_deref<std::unique_ptr<T>>());
+bool equal_targets(const Container<Own<T>>& a, const Container<Own<T>>& b) {
+    return equal_targets(a, b, comp_deref<Own<T>>());
 }
 
 /**
@@ -462,11 +250,31 @@
  * targets.
  */
 template <typename Key, typename Value>
-bool equal_targets(
-        const std::map<Key, std::unique_ptr<Value>>& a, const std::map<Key, std::unique_ptr<Value>>& b) {
-    auto comp = comp_deref<std::unique_ptr<Value>>();
+bool equal_targets(const std::map<Key, Own<Value>>& a, const std::map<Key, Own<Value>>& b) {
+    auto comp = comp_deref<Own<Value>>();
     return equal_targets(
             a, b, [&comp](auto& a, auto& b) { return a.first == b.first && comp(a.second, b.second); });
 }
 
+// -------------------------------------------------------------------------------
+//                             Checking Utilities
+// -------------------------------------------------------------------------------
+template <typename R>
+bool allValidPtrs(R const& range) {
+    return all(makeTransformRange(range, [](auto const& ptr) { return ptr != nullptr; }));
+}
+
 }  // namespace souffle
+
+namespace std {
+template <typename Iter, typename F>
+struct iterator_traits<souffle::TransformIterator<Iter, F>> {
+    using iter_t = std::iterator_traits<Iter>;
+    using iter_tag = typename iter_t::iterator_category;
+    using difference_type = typename iter_t::difference_type;
+    using reference = decltype(std::declval<F&>()(*std::declval<Iter>()));
+    using value_type = std::remove_cv_t<std::remove_reference_t<reference>>;
+    using iterator_category = std::conditional_t<std::is_base_of_v<std::random_access_iterator_tag, iter_tag>,
+            std::random_access_iterator_tag, iter_tag>;
+};
+}  // namespace std
diff --git a/cbits/souffle/utility/EvaluatorUtil.h b/cbits/souffle/utility/EvaluatorUtil.h
--- a/cbits/souffle/utility/EvaluatorUtil.h
+++ b/cbits/souffle/utility/EvaluatorUtil.h
@@ -16,9 +16,10 @@
 
 #pragma once
 
-#include "souffle/CompiledTuple.h"
 #include "souffle/RamTypes.h"
-#include "tinyformat.h"
+#include "souffle/utility/StringUtil.h"
+#include "souffle/utility/tinyformat.h"
+#include <csignal>
 
 namespace souffle::evaluator {
 
@@ -54,7 +55,10 @@
             return RamSignedFromString(src);
         } else if constexpr (std::is_same_v<RamUnsigned, A>) {
             return RamUnsignedFromString(src);
+        } else {
+            static_assert(sizeof(A) == 0, "Invalid type specified for symbol2Numeric");
         }
+
     } catch (...) {
         tfm::format(std::cerr, "error: wrong string provided by `to_number(\"%s\")` functor.\n", src);
         raise(SIGFPE);
diff --git a/cbits/souffle/utility/FileUtil.h b/cbits/souffle/utility/FileUtil.h
--- a/cbits/souffle/utility/FileUtil.h
+++ b/cbits/souffle/utility/FileUtil.h
@@ -1,6 +1,6 @@
 /*
  * Souffle - A Datalog Compiler
- * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved
+ * Copyright (c) 2021, The Souffle Developers. All rights reserved
  * Licensed under the Universal Permissive License v 1.0 as shown at:
  * - https://opensource.org/licenses/UPL
  * - <souffle root>/licenses/SOUFFLE-UPL.txt
@@ -127,12 +127,12 @@
     if (name.empty()) {
         return ".";
     }
-    size_t lastNotSlash = name.find_last_not_of('/');
+    std::size_t lastNotSlash = name.find_last_not_of('/');
     // All '/'
     if (lastNotSlash == std::string::npos) {
         return "/";
     }
-    size_t leadingSlash = name.find_last_of('/', lastNotSlash);
+    std::size_t leadingSlash = name.find_last_of('/', lastNotSlash);
     // No '/'
     if (leadingSlash == std::string::npos) {
         return ".";
@@ -195,14 +195,14 @@
         return ".";
     }
 
-    size_t lastNotSlash = filename.find_last_not_of('/');
+    std::size_t lastNotSlash = filename.find_last_not_of('/');
     if (lastNotSlash == std::string::npos) {
         return "/";
     }
 
-    size_t lastSlashBeforeBasename = filename.find_last_of('/', lastNotSlash - 1);
+    std::size_t lastSlashBeforeBasename = filename.find_last_of('/', lastNotSlash - 1);
     if (lastSlashBeforeBasename == std::string::npos) {
-        lastSlashBeforeBasename = static_cast<size_t>(-1);
+        lastSlashBeforeBasename = static_cast<std::size_t>(-1);
     }
     return filename.substr(lastSlashBeforeBasename + 1, lastNotSlash - lastSlashBeforeBasename);
 }
@@ -212,12 +212,12 @@
  */
 inline std::string simpleName(const std::string& path) {
     std::string name = baseName(path);
-    const size_t lastDot = name.find_last_of('.');
+    const std::size_t lastDot = name.find_last_of('.');
     // file has no extension
     if (lastDot == std::string::npos) {
         return name;
     }
-    const size_t lastSlash = name.find_last_of('/');
+    const std::size_t lastSlash = name.find_last_of('/');
     // last slash occurs after last dot, so no extension
     if (lastSlash != std::string::npos && lastSlash > lastDot) {
         return name;
@@ -231,12 +231,12 @@
  */
 inline std::string fileExtension(const std::string& path) {
     std::string name = path;
-    const size_t lastDot = name.find_last_of('.');
+    const std::size_t lastDot = name.find_last_of('.');
     // file has no extension
     if (lastDot == std::string::npos) {
         return std::string();
     }
-    const size_t lastSlash = name.find_last_of('/');
+    const std::size_t lastSlash = name.find_last_of('/');
     // last slash occurs after last dot, so no extension
     if (lastSlash != std::string::npos && lastSlash > lastDot) {
         return std::string();
diff --git a/cbits/souffle/utility/FunctionalUtil.h b/cbits/souffle/utility/FunctionalUtil.h
--- a/cbits/souffle/utility/FunctionalUtil.h
+++ b/cbits/souffle/utility/FunctionalUtil.h
@@ -1,6 +1,6 @@
 /*
  * Souffle - A Datalog Compiler
- * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved
+ * Copyright (c) 2021, The Souffle Developers. All rights reserved
  * Licensed under the Universal Permissive License v 1.0 as shown at:
  * - https://opensource.org/licenses/UPL
  * - <souffle root>/licenses/SOUFFLE-UPL.txt
@@ -18,6 +18,7 @@
 
 #include <algorithm>
 #include <functional>
+#include <set>
 #include <utility>
 #include <vector>
 
@@ -148,6 +149,18 @@
 template <typename A, typename F>
 std::vector<A> filter(std::vector<A> xs, F&& f) {
     return filterNot(std::move(xs), [&](auto&& x) { return !f(x); });
+}
+
+// -------------------------------------------------------------------------------
+//                               Set Utilities
+// -------------------------------------------------------------------------------
+
+template <typename A>
+std::set<A> operator-(const std::set<A>& lhs, const std::set<A>& rhs) {
+    std::set<A> result;
+    std::set_difference(
+            lhs.begin(), lhs.end(), rhs.begin(), rhs.end(), std::inserter(result, result.begin()));
+    return result;
 }
 
 }  // namespace souffle
diff --git a/cbits/souffle/utility/Iteration.h b/cbits/souffle/utility/Iteration.h
new file mode 100644
--- /dev/null
+++ b/cbits/souffle/utility/Iteration.h
@@ -0,0 +1,374 @@
+/*
+ * Souffle - A Datalog Compiler
+ * Copyright (c) 2020, The Souffle Developers. All rights reserved
+ * Licensed under the Universal Permissive License v 1.0 as shown at:
+ * - https://opensource.org/licenses/UPL
+ * - <souffle root>/licenses/SOUFFLE-UPL.txt
+ */
+
+/************************************************************************
+ *
+ * @file Iteration.h
+ *
+ * @brief Utilities for iterators and ranges
+ *
+ ***********************************************************************/
+
+#pragma once
+
+#include "souffle/utility/Types.h"
+
+#include <iterator>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+namespace souffle {
+
+namespace detail {
+
+// This is a helper in the cases when the lambda is stateless
+template <typename F>
+F const& makeFun() {
+    // Even thought the lambda is stateless, it has no default ctor
+    // Is this gross?  Yes, yes it is.
+    // FIXME: Remove after C++20
+    typename std::aligned_storage<sizeof(F)>::type fakeLam;
+    return reinterpret_cast<F const&>(fakeLam);
+}
+}  // namespace detail
+
+// -------------------------------------------------------------
+//                            Iterators
+// -------------------------------------------------------------
+/**
+ * A wrapper for an iterator that transforms values returned by
+ * the underlying iter.
+ *
+ * @tparam Iter ... the type of wrapped iterator
+ * @tparam F    ... the function to apply
+ *
+ */
+template <typename Iter, typename F>
+class TransformIterator {
+    using iter_t = std::iterator_traits<Iter>;
+    using difference_type = typename iter_t::difference_type;
+    using reference = decltype(std::declval<F&>()(*std::declval<Iter>()));
+    static_assert(std::is_empty_v<F>, "Function object must be stateless");
+
+public:
+    // some constructors
+    template <typename It>
+    TransformIterator(It iter, std::enable_if_t<std::is_empty_v<F>, void*> = nullptr)
+            : iter(std::move(iter)), fun(detail::makeFun<F>()) {}
+    TransformIterator(Iter iter, F f) : iter(std::move(iter)), fun(std::move(f)) {}
+
+    // defaulted copy and move constructors
+    TransformIterator(const TransformIterator& other) : iter(other.iter), fun(other.fun) {}
+    TransformIterator(TransformIterator&& other) : iter(std::move(other.iter)), fun(std::move(other.fun)) {}
+
+    // default assignment operators
+    TransformIterator& operator=(const TransformIterator& other) {
+        if (this != &other) {
+            iter = other.iter;
+        }
+        return *this;
+    }
+
+    TransformIterator& operator=(TransformIterator&& other) {
+        if (this != &other) {
+            iter = std::move(other.iter);
+        }
+        return *this;
+    }
+
+    /* The equality operator as required by the iterator concept. */
+    bool operator==(const TransformIterator& other) const {
+        return iter == other.iter;
+    }
+
+    /* The not-equality operator as required by the iterator concept. */
+    bool operator!=(const TransformIterator& other) const {
+        return iter != other.iter;
+    }
+
+    bool operator<(TransformIterator const& other) const {
+        return iter < other.iter;
+    }
+
+    bool operator<=(TransformIterator const& other) const {
+        return iter <= other.iter;
+    }
+
+    bool operator>(TransformIterator const& other) const {
+        return iter > other.iter;
+    }
+
+    bool operator>=(TransformIterator const& other) const {
+        return iter >= other.iter;
+    }
+
+    /* The deref operator as required by the iterator concept. */
+    auto operator*() const -> reference {
+        return fun(*iter);
+    }
+
+    /* Support for the pointer operator. */
+    auto operator->() const {
+        return &**this;
+    }
+
+    /* The increment operator as required by the iterator concept. */
+    TransformIterator& operator++() {
+        ++iter;
+        return *this;
+    }
+
+    TransformIterator operator++(int) {
+        auto res = *this;
+        ++iter;
+        return res;
+    }
+
+    TransformIterator& operator--() {
+        --iter;
+        return *this;
+    }
+
+    TransformIterator operator--(int) {
+        auto res = *this;
+        --iter;
+        return res;
+    }
+
+    TransformIterator& operator+=(difference_type n) {
+        iter += n;
+        return *this;
+    }
+
+    TransformIterator operator+(difference_type n) {
+        auto res = *this;
+        res += n;
+        return res;
+    }
+
+    TransformIterator& operator-=(difference_type n) {
+        iter -= n;
+        return *this;
+    }
+
+    TransformIterator operator-(difference_type n) {
+        auto res = *this;
+        res -= n;
+        return res;
+    }
+
+    difference_type operator-(TransformIterator const& other) {
+        return iter - other.iter;
+    }
+
+    auto operator[](difference_type ii) const -> reference {
+        return f(iter[ii]);
+    }
+
+private:
+    /* The nested iterator. */
+    Iter iter;
+    F fun;
+};
+
+template <typename Iter, typename F>
+auto operator+(
+        typename TransformIterator<Iter, F>::difference_type n, TransformIterator<Iter, F> const& iter) {
+    return iter + n;
+}
+
+template <typename Iter, typename F>
+auto transformIter(Iter&& iter, F&& f) {
+    return TransformIterator<remove_cvref_t<Iter>, std::remove_reference_t<F>>(
+            std::forward<Iter>(iter), std::forward<F>(f));
+}
+
+/**
+ * A wrapper for an iterator obtaining pointers of a certain type,
+ * dereferencing values before forwarding them to the consumer.
+ */
+namespace detail {
+inline auto iterDeref = [](auto& p) -> decltype(*p) { return *p; };
+}
+
+template <typename Iter>
+using IterDerefWrapper = TransformIterator<Iter, decltype(detail::iterDeref)>;
+
+/**
+ * A factory function enabling the construction of a dereferencing
+ * iterator utilizing the automated deduction of template parameters.
+ */
+template <typename Iter>
+auto derefIter(Iter&& iter) {
+    return transformIter(std::forward<Iter>(iter), detail::iterDeref);
+}
+
+// -------------------------------------------------------------
+//                             Ranges
+// -------------------------------------------------------------
+
+/**
+ * A utility class enabling representation of ranges by pairing
+ * two iterator instances marking lower and upper boundaries.
+ */
+template <typename Iter>
+struct range {
+    // the lower and upper boundary
+    Iter a, b;
+
+    // a constructor accepting a lower and upper boundary
+    range(Iter a, Iter b) : a(std::move(a)), b(std::move(b)) {}
+
+    // default copy / move and assignment support
+    range(const range&) = default;
+    range(range&&) = default;
+    range& operator=(const range&) = default;
+
+    // get the lower boundary (for for-all loop)
+    Iter& begin() {
+        return a;
+    }
+    const Iter& begin() const {
+        return a;
+    }
+
+    // get the upper boundary (for for-all loop)
+    Iter& end() {
+        return b;
+    }
+    const Iter& end() const {
+        return b;
+    }
+
+    // emptiness check
+    bool empty() const {
+        return a == b;
+    }
+
+    // splits up this range into the given number of partitions
+    std::vector<range> partition(int np = 100) {
+        // obtain the size
+        int n = 0;
+        for (auto i = a; i != b; ++i) {
+            n++;
+        }
+
+        // split it up
+        auto s = n / np;
+        auto r = n % np;
+        std::vector<range> res;
+        res.reserve(np);
+        auto cur = a;
+        auto last = cur;
+        int i = 0;
+        int p = 0;
+        while (cur != b) {
+            ++cur;
+            i++;
+            if (i >= (s + (p < r ? 1 : 0))) {
+                res.push_back({last, cur});
+                last = cur;
+                p++;
+                i = 0;
+            }
+        }
+        if (cur != last) {
+            res.push_back({last, cur});
+        }
+        return res;
+    }
+};
+
+/**
+ * A utility function enabling the construction of ranges
+ * without explicitly specifying the iterator type.
+ *
+ * @tparam Iter .. the iterator type
+ * @param a .. the lower boundary
+ * @param b .. the upper boundary
+ */
+template <typename Iter>
+range<Iter> make_range(const Iter& a, const Iter& b) {
+    return range<Iter>(a, b);
+}
+
+template <typename Iter, typename F>
+auto makeTransformRange(Iter&& begin, Iter&& end, F const& f) {
+    return make_range(transformIter(std::forward<Iter>(begin), f), transformIter(std::forward<Iter>(end), f));
+}
+
+template <typename R, typename F>
+auto makeTransformRange(R&& range, F const& f) {
+    return makeTransformRange(range.begin(), range.end(), f);
+}
+
+template <typename Iter>
+auto makeDerefRange(Iter&& begin, Iter&& end) {
+    return make_range(derefIter(std::forward<Iter>(begin)), derefIter(std::forward<Iter>(end)));
+}
+
+/**
+ * This wraps the Range container, and const_casts in place.
+ */
+template <typename Range, typename F>
+class OwningTransformRange {
+public:
+    OwningTransformRange(Range&& range, F f) : range(std::move(range)), f(std::move(f)) {}
+
+    auto begin() {
+        return transformIter(std::begin(range), f);
+    }
+
+    auto begin() const {
+        return transformIter(std::begin(range), f);
+    }
+
+    auto cbegin() const {
+        return transformIter(std::cbegin(range), f);
+    }
+
+    auto end() {
+        return transformIter(std::end(range), f);
+    }
+
+    auto end() const {
+        return transformIter(std::begin(range), f);
+    }
+
+    auto cend() const {
+        return transformIter(std::cend(range), f);
+    }
+
+    auto size() const {
+        return range.size();
+    }
+
+    auto& operator[](std::size_t ii) {
+        return begin()[ii];
+    }
+
+    auto& operator[](std::size_t ii) const {
+        return cbegin()[ii];
+    }
+
+private:
+    Range range;
+    F f;
+};
+
+/**
+ * Convert a range of any ptr-like to a range
+ * of pointers
+ */
+template <typename R>
+auto makePtrRange(R const& range) {
+    return makeTransformRange(range, [](auto const& ptrLike) { return &*ptrLike; });
+}
+
+}  // namespace souffle
diff --git a/cbits/souffle/utility/MiscUtil.h b/cbits/souffle/utility/MiscUtil.h
--- a/cbits/souffle/utility/MiscUtil.h
+++ b/cbits/souffle/utility/MiscUtil.h
@@ -1,6 +1,6 @@
 /*
  * Souffle - A Datalog Compiler
- * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved
+ * Copyright (c) 2021, The Souffle Developers. All rights reserved
  * Licensed under the Universal Permissive License v 1.0 as shown at:
  * - https://opensource.org/licenses/UPL
  * - <souffle root>/licenses/SOUFFLE-UPL.txt
@@ -16,10 +16,11 @@
 
 #pragma once
 
+#include "souffle/utility/Iteration.h"
+#include "souffle/utility/Types.h"
 #include "tinyformat.h"
 #include <cassert>
 #include <chrono>
-#include <cstdlib>
 #include <iostream>
 #include <memory>
 #include <utility>
@@ -48,7 +49,7 @@
  */
 #define __builtin_popcountll __popcnt64
 
-#if _MSC_VER < 1924
+#if defined(_MSC_VER)
 constexpr unsigned long __builtin_ctz(unsigned long value) {
     unsigned long trailing_zeroes = 0;
     while ((value = value >> 1) ^ 1) {
@@ -66,8 +67,8 @@
         return 64;
     }
 }
-#endif  // _MSC_VER < 1924
-#endif
+#endif  // _MSC_VER
+#endif  // _WIN32
 
 // -------------------------------------------------------------------------------
 //                               Timing Utils
@@ -98,16 +99,61 @@
 //                             Cloning Utilities
 // -------------------------------------------------------------------------------
 
+namespace detail {
+// TODO: This function is still used by ram::Node::clone() because it hasn't been
+// converted to return Own<>.  Once converted, remove this.
+template <typename D, typename B>
+Own<D> downCast(B* ptr) {
+    // ensure the clone operation casts to appropriate pointer
+    static_assert(std::is_base_of_v<std::remove_const_t<B>, std::remove_const_t<D>>,
+            "Needs to be able to downcast");
+    return Own<D>(ptr);
+}
+
+template <typename D, typename B>
+Own<D> downCast(Own<B> ptr) {
+    // ensure the clone operation casts to appropriate pointer
+    static_assert(std::is_base_of_v<std::remove_const_t<B>, std::remove_const_t<D>>,
+            "Needs to be able to downcast");
+    return Own<D>(static_cast<D*>(ptr.release()));
+}
+
+}  // namespace detail
+
 template <typename A>
-std::unique_ptr<A> clone(const A* node) {
-    return node ? std::unique_ptr<A>(node->clone()) : nullptr;
+std::enable_if_t<!std::is_pointer_v<A> && !is_range_v<A>, Own<A>> clone(const A& node) {
+    return detail::downCast<A>(node.cloneImpl());
 }
 
 template <typename A>
-std::unique_ptr<A> clone(const std::unique_ptr<A>& node) {
-    return node ? std::unique_ptr<A>(node->clone()) : nullptr;
+Own<A> clone(const A* node) {
+    return node ? clone(*node) : nullptr;
 }
 
+template <typename A>
+Own<A> clone(const Own<A>& node) {
+    return clone(node.get());
+}
+
+/**
+ * Clone a range
+ */
+template <typename R>
+auto cloneRange(R const& range) {
+    return makeTransformRange(std::begin(range), std::end(range), [](auto const& x) { return clone(x); });
+}
+
+/**
+ * Clone a range, optionally allowing up-casting the result to D
+ */
+template <typename D = void, typename R, std::enable_if_t<is_range_v<R>, void*> = nullptr>
+auto clone(R const& range) {
+    auto rn = cloneRange(range);
+    using ValueType = remove_cvref_t<decltype(**std::begin(range))>;
+    using ResType = std::conditional_t<std::is_same_v<D, void>, ValueType, D>;
+    return VecOwn<ResType>(rn.begin(), rn.end());
+}
+
 template <typename A, typename B>
 auto clone(const std::pair<A, B>& p) {
     return std::make_pair(clone(p.first), clone(p.second));
@@ -136,54 +182,79 @@
  * pointers are null is also considered equivalent.
  */
 template <typename T>
-bool equal_ptr(const std::unique_ptr<T>& a, const std::unique_ptr<T>& b) {
+bool equal_ptr(const Own<T>& a, const Own<T>& b) {
     return equal_ptr(a.get(), b.get());
 }
 
-template <typename A, typename B>
-using copy_const_t = std::conditional_t<std::is_const_v<A>, const B, B>;
+/**
+ * This class is used to tell as<> that cross-casting is allowed.
+ * I use a named type rather than just a bool to make the code stand out.
+ */
+class AllowCrossCast {};
 
 /**
  * Helpers for `dynamic_cast`ing without having to specify redundant type qualifiers.
- * e.g. `as<AstLiteral>(p)` instead of `dynamic_cast<const AstLiteral*>(p.get())`.
+ * e.g. `as<AstLiteral>(p)` instead of `as<AstLiteral>(p)`.
  */
-template <typename B, typename A>
+template <typename B, typename CastType = void, typename A>
 auto as(A* x) {
-    static_assert(std::is_base_of_v<A, B>,
-            "`as<B, A>` does not allow cross-type dyn casts. "
-            "(i.e. `as<B, A>` where `B <: A` is not true.) "
-            "Such a cast is likely a mistake or typo.");
+    if constexpr (!std::is_same_v<CastType, AllowCrossCast>) {
+        static_assert(std::is_base_of_v<std::remove_const_t<A>, std::remove_const_t<B>>,
+                "`as<B, A>` does not allow cross-type dyn casts. "
+                "(i.e. `as<B, A>` where `B <: A` is not true.) "
+                "Such a cast is likely a mistake or typo.");
+    }
     return dynamic_cast<copy_const_t<A, B>*>(x);
 }
 
-template <typename B, typename A>
-std::enable_if_t<std::is_base_of_v<A, B>, copy_const_t<A, B>*> as(A& x) {
-    return as<B>(&x);
+template <typename B, typename CastType = void, typename A>
+auto as(A& x) {
+    return as<B, CastType>(&x);
 }
 
-template <typename B, typename A>
-B* as(const std::unique_ptr<A>& x) {
-    return as<B>(x.get());
+template <typename B, typename CastType = void, typename A>
+auto as(Own<A>& x) {
+    return as<B, CastType>(x.get());
 }
 
+template <typename B, typename CastType = void, typename A>
+auto as(const Own<A>& x) {
+    return as<B, CastType>(x.get());
+}
+
 /**
+ * Down-casts and checks the cast has succeeded
+ */
+template <typename B, typename CastType = void, typename A>
+auto& asAssert(A&& a) {
+    auto* cast = as<B, CastType>(std::forward<A>(a));
+    assert(cast && "Invalid cast");
+    return *cast;
+}
+
+/**
  * Checks if the object of type Source can be casted to type Destination.
  */
 template <typename B, typename A>
 bool isA(A* x) {
+    // Dont't forward onto as<> - need to check cross-casting
     return dynamic_cast<copy_const_t<A, B>*>(x) != nullptr;
 }
 
 template <typename B, typename A>
-std::enable_if_t<std::is_base_of_v<A, B>, bool> isA(A& x) {
+auto isA(A& x) {
     return isA<B>(&x);
 }
 
 template <typename B, typename A>
-bool isA(const std::unique_ptr<A>& x) {
+bool isA(const Own<A>& x) {
     return isA<B>(x.get());
 }
 
+template <typename B, typename A>
+bool isA(Own<A>& x) {
+    return isA<B>(x.get());
+}
 // -------------------------------------------------------------------------------
 //                               Error Utilities
 // -------------------------------------------------------------------------------
diff --git a/cbits/souffle/utility/ParallelUtil.h b/cbits/souffle/utility/ParallelUtil.h
--- a/cbits/souffle/utility/ParallelUtil.h
+++ b/cbits/souffle/utility/ParallelUtil.h
@@ -18,7 +18,22 @@
 #pragma once
 
 #include <atomic>
+#include <cassert>
+#include <cstddef>
+#include <memory>
+#include <new>
 
+#if defined(__cpp_lib_hardware_interference_size) && \
+        (!defined(__APPLE__))  // https://bugs.llvm.org/show_bug.cgi?id=41423
+using std::hardware_constructive_interference_size;
+using std::hardware_destructive_interference_size;
+#else
+// 64 bytes on x86-64 │ L1_CACHE_BYTES │ L1_CACHE_SHIFT │ __cacheline_aligned │
+// ...
+constexpr std::size_t hardware_constructive_interference_size = 2 * sizeof(max_align_t);
+constexpr std::size_t hardware_destructive_interference_size = 2 * sizeof(max_align_t);
+#endif
+
 #ifdef _OPENMP
 
 /**
@@ -55,7 +70,7 @@
 #define SECTION_END }
 
 // a macro to create an operation context
-#define CREATE_OP_CONTEXT(NAME, INIT) auto NAME = INIT;
+#define CREATE_OP_CONTEXT(NAME, INIT) [[maybe_unused]] auto NAME = INIT;
 #define READ_OP_CONTEXT(NAME) NAME
 
 #else
@@ -80,7 +95,7 @@
 #define SECTION_END }
 
 // a macro to create an operation context
-#define CREATE_OP_CONTEXT(NAME, INIT) auto NAME = INIT;
+#define CREATE_OP_CONTEXT(NAME, INIT) [[maybe_unused]] auto NAME = INIT;
 #define READ_OP_CONTEXT(NAME) NAME
 
 // mark es sequential
@@ -93,17 +108,66 @@
 #endif
 
 #ifdef IS_PARALLEL
+#include <mutex>
+#include <vector>
 #define MAX_THREADS (omp_get_max_threads())
 #else
 #define MAX_THREADS (1)
 #endif
 
-#ifdef IS_PARALLEL
+namespace souffle {
 
-#include <mutex>
+struct SeqConcurrentLanes {
+    struct TrivialLock {
+        ~TrivialLock() {}
+    };
 
-namespace souffle {
+    using lane_id = std::size_t;
+    using unique_lock_type = TrivialLock;
 
+    explicit SeqConcurrentLanes(std::size_t = 1) {}
+    SeqConcurrentLanes(const SeqConcurrentLanes&) = delete;
+    SeqConcurrentLanes(SeqConcurrentLanes&&) = delete;
+
+    virtual ~SeqConcurrentLanes() {}
+
+    std::size_t lanes() const {
+        return 1;
+    }
+
+    void setNumLanes(const std::size_t) {}
+
+    unique_lock_type guard(const lane_id) const {
+        return TrivialLock();
+    }
+
+    void lock(const lane_id) const {
+        return;
+    }
+
+    void unlock(const lane_id) const {
+        return;
+    }
+
+    void beforeLockAllBut(const lane_id) const {
+        return;
+    }
+
+    void beforeUnlockAllBut(const lane_id) const {
+        return;
+    }
+
+    void lockAllBut(const lane_id) const {
+        return;
+    }
+
+    void unlockAllBut(const lane_id) const {
+        return;
+    }
+};
+
+#ifdef IS_PARALLEL
+
 /**
  * A small utility class for implementing simple locks.
  */
@@ -457,10 +521,190 @@
     }
 };
 
-#else
+/** Concurrent tracks locking mechanism. */
+struct MutexConcurrentLanes {
+    using lane_id = std::size_t;
+    using unique_lock_type = std::unique_lock<std::mutex>;
 
-namespace souffle {
+    explicit MutexConcurrentLanes(const std::size_t Sz) : Size(Sz), Attribution(attribution(Sz)) {
+        Lanes = std::make_unique<Lane[]>(Sz);
+    }
+    MutexConcurrentLanes(const MutexConcurrentLanes&) = delete;
+    MutexConcurrentLanes(MutexConcurrentLanes&&) = delete;
 
+    virtual ~MutexConcurrentLanes() {}
+
+    // Return the number of lanes.
+    std::size_t lanes() const {
+        return Size;
+    }
+
+    // Select a lane
+    lane_id getLane(std::size_t I) const {
+        if (Attribution == lane_attribution::mod_power_of_2) {
+            return I & (Size - 1);
+        } else {
+            return I % Size;
+        }
+    }
+
+    /** Change the number of lanes.
+     * DO not use while threads are using this object.
+     */
+    void setNumLanes(const std::size_t NumLanes) {
+        Size = (NumLanes == 0 ? 1 : NumLanes);
+        Attribution = attribution(Size);
+        Lanes = std::make_unique<Lane[]>(Size);
+    }
+
+    unique_lock_type guard(const lane_id Lane) const {
+        return unique_lock_type(Lanes[Lane].Access);
+    }
+
+    // Lock the given track.
+    // Must eventually be followed by unlock(Lane).
+    void lock(const lane_id Lane) const {
+        assert(Lane < Size);
+        Lanes[Lane].Access.lock();
+    }
+
+    // Unlock the given track.
+    // Must already be the owner of the track's lock.
+    void unlock(const lane_id Lane) const {
+        assert(Lane < Size);
+        Lanes[Lane].Access.unlock();
+    }
+
+    // Acquire the capability to lock all other tracks than the given one.
+    //
+    // Must eventually be followed by beforeUnlockAllBut(Lane).
+    void beforeLockAllBut(const lane_id Lane) const {
+        if (!BeforeLockAll.try_lock()) {
+            // If we cannot get the lock immediately, it means it was acquired
+            // concurrently by another track that will also try to acquire our
+            // track lock.
+            // So we release our track lock to let the concurrent operation
+            // progress.
+            unlock(Lane);
+            BeforeLockAll.lock();
+            lock(Lane);
+        }
+    }
+
+    // Release the capability to lock all other tracks than the given one.
+    //
+    // Must already be the owner of that capability.
+    void beforeUnlockAllBut(const lane_id) const {
+        BeforeLockAll.unlock();
+    }
+
+    // Lock all tracks but the given one.
+    //
+    // Must already have acquired the capability to lock all other tracks
+    // by calling beforeLockAllBut(Lane).
+    //
+    // Must eventually be followed by unlockAllBut(Lane).
+    void lockAllBut(const lane_id Lane) const {
+        for (std::size_t I = 0; I < Size; ++I) {
+            if (I != Lane) {
+                Lanes[I].Access.lock();
+            }
+        }
+    }
+
+    // Unlock all tracks but the given one.
+    // Must already be the owner of all the tracks' locks.
+    void unlockAllBut(const lane_id Lane) const {
+        for (std::size_t I = 0; I < Size; ++I) {
+            if (I != Lane) {
+                Lanes[I].Access.unlock();
+            }
+        }
+    }
+
+private:
+    enum lane_attribution { mod_power_of_2, mod_other };
+
+    struct Lane {
+        alignas(hardware_destructive_interference_size) std::mutex Access;
+    };
+
+    static constexpr lane_attribution attribution(const std::size_t Sz) {
+        assert(Sz > 0);
+        if ((Sz & (Sz - 1)) == 0) {
+            // Sz is a power of 2
+            return lane_attribution::mod_power_of_2;
+        } else {
+            return lane_attribution::mod_other;
+        }
+    }
+
+protected:
+    std::size_t Size;
+    lane_attribution Attribution;
+
+private:
+    mutable std::unique_ptr<Lane[]> Lanes;
+
+    alignas(hardware_destructive_interference_size) mutable std::mutex BeforeLockAll;
+};
+
+class ConcurrentLanes : public MutexConcurrentLanes {
+    using Base = MutexConcurrentLanes;
+
+public:
+    using lane_id = Base::lane_id;
+    using Base::beforeLockAllBut;
+    using Base::beforeUnlockAllBut;
+    using Base::guard;
+    using Base::lock;
+    using Base::lockAllBut;
+    using Base::unlock;
+    using Base::unlockAllBut;
+
+    explicit ConcurrentLanes(const std::size_t Sz) : MutexConcurrentLanes(Sz) {}
+    ConcurrentLanes(const ConcurrentLanes&) = delete;
+    ConcurrentLanes(ConcurrentLanes&&) = delete;
+
+    lane_id threadLane() const {
+        return getLane(static_cast<std::size_t>(omp_get_thread_num()));
+    }
+
+    void setNumLanes(const std::size_t NumLanes) {
+        Base::setNumLanes(NumLanes == 0 ? omp_get_max_threads() : NumLanes);
+    }
+
+    unique_lock_type guard() const {
+        return Base::guard(threadLane());
+    }
+
+    void lock() const {
+        return Base::lock(threadLane());
+    }
+
+    void unlock() const {
+        return Base::unlock(threadLane());
+    }
+
+    void beforeLockAllBut() const {
+        return Base::beforeLockAllBut(threadLane());
+    }
+
+    void beforeUnlockAllBut() const {
+        return Base::beforeUnlockAllBut(threadLane());
+    }
+
+    void lockAllBut() const {
+        return Base::lockAllBut(threadLane());
+    }
+
+    void unlockAllBut() const {
+        return Base::unlockAllBut(threadLane());
+    }
+};
+
+#else
+
 /**
  * A small utility class for implementing simple locks.
  */
@@ -560,6 +804,53 @@
     }
 };
 
+struct ConcurrentLanes : protected SeqConcurrentLanes {
+    using Base = SeqConcurrentLanes;
+    using lane_id = SeqConcurrentLanes::lane_id;
+    using unique_lock_type = SeqConcurrentLanes::unique_lock_type;
+
+    using Base::lanes;
+    using Base::setNumLanes;
+
+    explicit ConcurrentLanes(std::size_t Sz = MAX_THREADS) : Base(Sz) {}
+    ConcurrentLanes(const ConcurrentLanes&) = delete;
+    ConcurrentLanes(ConcurrentLanes&&) = delete;
+
+    virtual ~ConcurrentLanes() {}
+
+    lane_id threadLane() const {
+        return 0;
+    }
+
+    unique_lock_type guard() const {
+        return Base::guard(threadLane());
+    }
+
+    void lock() const {
+        return Base::lock(threadLane());
+    }
+
+    void unlock() const {
+        return Base::unlock(threadLane());
+    }
+
+    void beforeLockAllBut() const {
+        return Base::beforeLockAllBut(threadLane());
+    }
+
+    void beforeUnlockAllBut() const {
+        return Base::beforeUnlockAllBut(threadLane());
+    }
+
+    void lockAllBut() const {
+        return Base::lockAllBut(threadLane());
+    }
+
+    void unlockAllBut() const {
+        return Base::unlockAllBut(threadLane());
+    }
+};
+
 #endif
 
 /**
@@ -570,4 +861,4 @@
     return outputLock;
 }
 
-}  // end of namespace souffle
+}  // namespace souffle
diff --git a/cbits/souffle/utility/StreamUtil.h b/cbits/souffle/utility/StreamUtil.h
--- a/cbits/souffle/utility/StreamUtil.h
+++ b/cbits/souffle/utility/StreamUtil.h
@@ -1,6 +1,6 @@
 /*
  * Souffle - A Datalog Compiler
- * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved
+ * Copyright (c) 2021, The Souffle Developers. All rights reserved
  * Licensed under the Universal Permissive License v 1.0 as shown at:
  * - https://opensource.org/licenses/UPL
  * - <souffle root>/licenses/SOUFFLE-UPL.txt
@@ -26,6 +26,7 @@
 #include <vector>
 
 #include "souffle/utility/ContainerUtil.h"
+#include "souffle/utility/span.h"
 
 // -------------------------------------------------------------------------------
 //                           General Print Utilities
@@ -33,6 +34,25 @@
 
 namespace souffle {
 
+// Usage:       `using namespace stream_write_qualified_char_as_number;`
+//              NB: `using` must appear in the same namespace as the `<<` callers.
+//                  Putting the `using` in a parent namespace will have no effect.
+// Motivation:  Octet sized numeric types are often defined as aliases of a qualified
+//              `char`. e.g. `using uint8_t = unsigned char'`
+//              `std::ostream` has an overload which converts qualified `char`s to plain `char`.
+//              You don't usually want to print a `uint8_t` as an ASCII character.
+//
+// NOTE:        `char`, `signed char`, and `unsigned char` are distinct types.
+namespace stream_write_qualified_char_as_number {
+inline std::ostream& operator<<(std::ostream& os, signed char c) {
+    return os << int(c);
+}
+
+inline std::ostream& operator<<(std::ostream& os, unsigned char c) {
+    return os << unsigned(c);
+}
+}  // namespace stream_write_qualified_char_as_number
+
 template <typename A>
 struct IsPtrLike : std::is_pointer<A> {};
 template <typename A>
@@ -186,14 +206,14 @@
  * For use cases see the test case {util_test.cpp}.
  */
 template <typename Container, typename Iter = typename Container::const_iterator,
-        typename T = typename Iter::value_type>
+        typename T = typename std::iterator_traits<Iter>::value_type>
 std::enable_if_t<!JoinShouldDeref<T>, detail::joined_sequence<Iter, detail::print<id<T>>>> join(
         const Container& c, const std::string& sep = ",") {
     return join(c.begin(), c.end(), sep, detail::print<id<T>>());
 }
 
 template <typename Container, typename Iter = typename Container::const_iterator,
-        typename T = typename Iter::value_type>
+        typename T = typename std::iterator_traits<Iter>::value_type>
 std::enable_if_t<JoinShouldDeref<T>, detail::joined_sequence<Iter, detail::print<deref<T>>>> join(
         const Container& c, const std::string& sep = ",") {
     return join(c.begin(), c.end(), sep, detail::print<deref<T>>());
@@ -206,6 +226,15 @@
 namespace std {
 
 /**
+ * Enables the generic printing of `array`s assuming their element types
+ * are printable.
+ */
+template <typename T, std::size_t E>
+ostream& operator<<(ostream& out, const array<T, E>& v) {
+    return out << "[" << souffle::join(v) << "]";
+}
+
+/**
  * Introduces support for printing pairs as long as their components can be printed.
  */
 template <typename A, typename B>
@@ -219,6 +248,15 @@
  */
 template <typename T, typename A>
 ostream& operator<<(ostream& out, const vector<T, A>& v) {
+    return out << "[" << souffle::join(v) << "]";
+}
+
+/**
+ * Enables the generic printing of `span`s assuming their element types
+ * are printable.
+ */
+template <typename T, std::size_t E>
+ostream& operator<<(ostream& out, const souffle::span<T, E>& v) {
     return out << "[" << souffle::join(v) << "]";
 }
 
diff --git a/cbits/souffle/utility/StringUtil.h b/cbits/souffle/utility/StringUtil.h
--- a/cbits/souffle/utility/StringUtil.h
+++ b/cbits/souffle/utility/StringUtil.h
@@ -1,6 +1,6 @@
 /*
  * Souffle - A Datalog Compiler
- * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved
+ * Copyright (c) 2021, The Souffle Developers. All rights reserved
  * Licensed under the Universal Permissive License v 1.0 as shown at:
  * - https://opensource.org/licenses/UPL
  * - <souffle root>/licenses/SOUFFLE-UPL.txt
@@ -154,7 +154,7 @@
  * starts with minus (c++ default semantics).
  */
 inline bool canBeParsedAsRamSigned(const std::string& string) {
-    size_t charactersRead = 0;
+    std::size_t charactersRead = 0;
 
     try {
         RamSignedFromString(string, &charactersRead, 0);
@@ -171,7 +171,7 @@
  * Souffle accepts: hex, binary and base 10.
  */
 inline bool canBeParsedAsRamUnsigned(const std::string& string) {
-    size_t charactersRead = 0;
+    std::size_t charactersRead = 0;
     try {
         RamUnsignedFromString(string, &charactersRead, 0);
     } catch (...) {
@@ -184,7 +184,7 @@
  * Can a string be parsed as RamFloat.
  */
 inline bool canBeParsedAsRamFloat(const std::string& string) {
-    size_t charactersRead = 0;
+    std::size_t charactersRead = 0;
     try {
         RamFloatFromString(string, &charactersRead);
     } catch (...) {
@@ -323,13 +323,20 @@
 }
 
 /**
+ * Strips the prefix of a given string if it exists. No change otherwise.
+ */
+inline std::string stripPrefix(const std::string& prefix, const std::string& element) {
+    return isPrefix(prefix, element) ? element.substr(prefix.length()) : element;
+}
+
+/**
  * Stringify a string using escapes for escape, newline, tab, double-quotes and semicolons
  */
 inline std::string stringify(const std::string& input) {
     std::string str(input);
 
     // replace escapes with double escape sequence
-    size_t start_pos = 0;
+    std::size_t start_pos = 0;
     while ((start_pos = str.find('\\', start_pos)) != std::string::npos) {
         str.replace(start_pos, 1, "\\\\");
         start_pos += 2;
@@ -379,7 +386,7 @@
 
 /** Valid C++ identifier, note that this does not ensure the uniqueness of identifiers returned. */
 inline std::string identifier(std::string id) {
-    for (size_t i = 0; i < id.length(); i++) {
+    for (std::size_t i = 0; i < id.length(); i++) {
         if (((isalpha(id[i]) == 0) && i == 0) || ((isalnum(id[i]) == 0) && id[i] != '_')) {
             id[i] = '_';
         }
@@ -392,7 +399,7 @@
 inline std::string unescape(
         const std::string& inputString, const std::string& needle, const std::string& replacement) {
     std::string result = inputString;
-    size_t pos = 0;
+    std::size_t pos = 0;
     while ((pos = result.find(needle, pos)) != std::string::npos) {
         result = result.replace(pos, needle.length(), replacement);
         pos += replacement.length();
@@ -411,7 +418,7 @@
 inline std::string escape(
         const std::string& inputString, const std::string& needle, const std::string& replacement) {
     std::string result = inputString;
-    size_t pos = 0;
+    std::size_t pos = 0;
     while ((pos = result.find(needle, pos)) != std::string::npos) {
         result = result.replace(pos, needle.length(), replacement);
         pos += replacement.length();
diff --git a/cbits/souffle/utility/Types.h b/cbits/souffle/utility/Types.h
new file mode 100644
--- /dev/null
+++ b/cbits/souffle/utility/Types.h
@@ -0,0 +1,88 @@
+/*
+ * Souffle - A Datalog Compiler
+ * Copyright (c) 2020, The Souffle Developers. All rights reserved
+ * Licensed under the Universal Permissive License v 1.0 as shown at:
+ * - https://opensource.org/licenses/UPL
+ * - <souffle root>/licenses/SOUFFLE-UPL.txt
+ */
+
+/************************************************************************
+ *
+ * @file Types.h
+ *
+ * @brief Shared type definitions
+ *
+ ***********************************************************************/
+
+#pragma once
+
+#include <memory>
+#include <type_traits>
+#include <vector>
+
+namespace souffle {
+template <typename A>
+using Own = std::unique_ptr<A>;
+
+template <typename A, typename B = A, typename... Args>
+Own<A> mk(Args&&... xs) {
+    return std::make_unique<B>(std::forward<Args>(xs)...);
+}
+
+template <typename A>
+using VecOwn = std::vector<Own<A>>;
+
+/**
+ * Copy the const qualifier of type T onto type U
+ */
+template <typename A, typename B>
+using copy_const = std::conditional<std::is_const_v<A>, const B, B>;
+
+template <typename A, typename B>
+using copy_const_t = typename copy_const<A, B>::type;
+
+namespace detail {
+template <typename T, typename U = void>
+struct is_range_impl : std::false_type {};
+
+template <typename T>
+struct is_range_impl<T, std::void_t<decltype(*std::begin(std::declval<T&>()))>> : std::true_type {};
+
+}  // namespace detail
+
+/**
+ * A simple test to check if T is a range (i.e. has std::begin())
+ */
+template <typename T>
+struct is_range : detail::is_range_impl<T> {};
+
+template <typename T>
+inline constexpr bool is_range_v = is_range<T>::value;
+
+/**
+ * Type identity, remove once we have C++20
+ */
+template <typename T>
+struct type_identity {
+    using type = T;
+};
+
+/**
+ * Remove cv ref, remove once we have C++ 20
+ */
+template <typename T>
+using remove_cvref = std::remove_cv<std::remove_reference_t<T>>;
+
+template <class T>
+using remove_cvref_t = typename remove_cvref<T>::type;
+
+template <typename T>
+struct is_pointer_like : std::is_pointer<T> {};
+
+template <typename T>
+struct is_pointer_like<Own<T>> : std::true_type {};
+
+template <typename T>
+inline constexpr bool is_pointer_like_v = is_pointer_like<T>::value;
+
+}  // namespace souffle
diff --git a/cbits/souffle/utility/json11.h b/cbits/souffle/utility/json11.h
--- a/cbits/souffle/utility/json11.h
+++ b/cbits/souffle/utility/json11.h
@@ -168,7 +168,7 @@
     const object& object_items() const;
 
     // Return a reference to arr[i] if this is an array, Json() otherwise.
-    const Json& operator[](size_t i) const;
+    const Json& operator[](std::size_t i) const;
     // Return a reference to obj[key] if this is an object, Json() otherwise.
     const Json& operator[](const std::string& key) const;
 
@@ -257,7 +257,7 @@
     virtual bool bool_value() const;
     virtual const std::string& string_value() const;
     virtual const Json::array& array_items() const;
-    virtual const Json& operator[](size_t i) const;
+    virtual const Json& operator[](std::size_t i) const;
     virtual const Json::object& object_items() const;
     virtual const Json& operator[](const std::string& key) const;
     virtual ~JsonValue() = default;
@@ -308,7 +308,7 @@
 
 static void dump(const std::string& value, std::string& out) {
     out += '"';
-    for (size_t i = 0; i < value.length(); i++) {
+    for (std::size_t i = 0; i < value.length(); i++) {
         const char ch = value[i];
         if (ch == '\\') {
             out += "\\\\";
@@ -465,7 +465,7 @@
     const Json::array& array_items() const override {
         return m_value;
     }
-    const Json& operator[](size_t i) const override;
+    const Json& operator[](std::size_t i) const override;
 
 public:
     explicit JsonArray(const Json::array& value) : Value(value) {}
@@ -557,7 +557,7 @@
 inline const std::map<std::string, Json>& Json::object_items() const {
     return m_ptr->object_items();
 }
-inline const Json& Json::operator[](size_t i) const {
+inline const Json& Json::operator[](std::size_t i) const {
     return (*m_ptr)[i];
 }
 inline const Json& Json::operator[](const std::string& key) const {
@@ -585,7 +585,7 @@
 inline const std::map<std::string, Json>& JsonValue::object_items() const {
     return statics().empty_map;
 }
-inline const Json& JsonValue::operator[](size_t) const {
+inline const Json& JsonValue::operator[](std::size_t) const {
     return static_null();
 }
 inline const Json& JsonValue::operator[](const std::string&) const {
@@ -596,7 +596,7 @@
     auto iter = m_value.find(key);
     return (iter == m_value.end()) ? static_null() : iter->second;
 }
-inline const Json& JsonArray::operator[](size_t i) const {
+inline const Json& JsonArray::operator[](std::size_t i) const {
     if (i >= m_value.size()) {
         return static_null();
     }
@@ -660,7 +660,7 @@
     /* State
      */
     const std::string& str;
-    size_t i;
+    std::size_t i;
     std::string& err;
     bool failed;
     const JsonParse strategy;
@@ -835,7 +835,7 @@
                 if (esc.length() < 4) {
                     return fail("bad \\u escape: " + esc, "");
                 }
-                for (size_t j = 0; j < 4; j++) {
+                for (std::size_t j = 0; j < 4; j++) {
                     if (!in_range(esc[j], 'a', 'f') && !in_range(esc[j], 'A', 'F') &&
                             !in_range(esc[j], '0', '9'))
                         return fail("bad \\u escape: " + esc, "");
@@ -888,7 +888,7 @@
      * Parse a double.
      */
     Json parse_number() {
-        size_t start_pos = i;
+        std::size_t start_pos = i;
 
         if (str[i] == '-') {
             i++;
@@ -910,7 +910,7 @@
         }
 
         if (str[i] != '.' && str[i] != 'e' && str[i] != 'E' &&
-                (i - start_pos) <= static_cast<size_t>(std::numeric_limits<int>::digits10)) {
+                (i - start_pos) <= static_cast<std::size_t>(std::numeric_limits<int>::digits10)) {
             return std::atoll(str.c_str() + start_pos);
         }
 
diff --git a/cbits/souffle/utility/span.h b/cbits/souffle/utility/span.h
new file mode 100644
--- /dev/null
+++ b/cbits/souffle/utility/span.h
@@ -0,0 +1,662 @@
+#pragma once
+
+#if __cplusplus >= 202000L
+
+#include <span>  // use std lib impl
+
+namespace souffle {
+constexpr auto dynamic_extent = std::dynamic_extent;
+
+template <typename A, std::size_t E = std::dynamic_extent>
+using span = std::span<A, E>;
+}  // namespace souffle
+
+#else
+
+// clang-format off
+/*
+This is an implementation of C++20's std::span
+http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/n4820.pdf
+*/
+
+//          Copyright Tristan Brindle 2018.
+// Distributed under the Boost Software License, Version 1.0.
+//    (See accompanying file ../../LICENSE_1_0.txt or copy at
+//          https://www.boost.org/LICENSE_1_0.txt)
+
+#ifndef TCB_SPAN_HPP_INCLUDED
+#define TCB_SPAN_HPP_INCLUDED
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <type_traits>
+
+#ifndef TCB_SPAN_NO_EXCEPTIONS
+// Attempt to discover whether we're being compiled with exception support
+#if !(defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND))
+#define TCB_SPAN_NO_EXCEPTIONS
+#endif
+#endif
+
+#ifndef TCB_SPAN_NO_EXCEPTIONS
+#include <cstdio>
+#include <stdexcept>
+#endif
+
+// Various feature test macros
+
+#ifndef TCB_SPAN_NAMESPACE_NAME
+#define TCB_SPAN_NAMESPACE_NAME tcb
+#endif
+
+#if __cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)
+#define TCB_SPAN_HAVE_CPP17
+#endif
+
+#if __cplusplus >= 201402L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201402L)
+#define TCB_SPAN_HAVE_CPP14
+#endif
+
+namespace TCB_SPAN_NAMESPACE_NAME {
+
+// Establish default contract checking behavior
+#if !defined(TCB_SPAN_THROW_ON_CONTRACT_VIOLATION) &&                          \
+    !defined(TCB_SPAN_TERMINATE_ON_CONTRACT_VIOLATION) &&                      \
+    !defined(TCB_SPAN_NO_CONTRACT_CHECKING)
+#if defined(NDEBUG) || !defined(TCB_SPAN_HAVE_CPP14)
+#define TCB_SPAN_NO_CONTRACT_CHECKING
+#else
+#define TCB_SPAN_TERMINATE_ON_CONTRACT_VIOLATION
+#endif
+#endif
+
+#if defined(TCB_SPAN_THROW_ON_CONTRACT_VIOLATION)
+struct contract_violation_error : std::logic_error {
+    explicit contract_violation_error(const char* msg) : std::logic_error(msg)
+    {}
+};
+
+inline void contract_violation(const char* msg)
+{
+    throw contract_violation_error(msg);
+}
+
+#elif defined(TCB_SPAN_TERMINATE_ON_CONTRACT_VIOLATION)
+[[noreturn]] inline void contract_violation(const char* /*unused*/)
+{
+    std::terminate();
+}
+#endif
+
+#if !defined(TCB_SPAN_NO_CONTRACT_CHECKING)
+#define TCB_SPAN_STRINGIFY(cond) #cond
+#define TCB_SPAN_EXPECT(cond)                                                  \
+    cond ? (void) 0 : contract_violation("Expected " TCB_SPAN_STRINGIFY(cond))
+#else
+#define TCB_SPAN_EXPECT(cond)
+#endif
+
+#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_inline_variables)
+#define TCB_SPAN_INLINE_VAR inline
+#else
+#define TCB_SPAN_INLINE_VAR
+#endif
+
+#if defined(TCB_SPAN_HAVE_CPP14) ||                                            \
+    (defined(__cpp_constexpr) && __cpp_constexpr >= 201304)
+#define TCB_SPAN_HAVE_CPP14_CONSTEXPR
+#endif
+
+#if defined(TCB_SPAN_HAVE_CPP14_CONSTEXPR)
+#define TCB_SPAN_CONSTEXPR14 constexpr
+#else
+#define TCB_SPAN_CONSTEXPR14
+#endif
+
+#if defined(TCB_SPAN_HAVE_CPP14_CONSTEXPR) &&                                  \
+    (!defined(_MSC_VER) || _MSC_VER > 1900)
+#define TCB_SPAN_CONSTEXPR_ASSIGN constexpr
+#else
+#define TCB_SPAN_CONSTEXPR_ASSIGN
+#endif
+
+#if defined(TCB_SPAN_NO_CONTRACT_CHECKING)
+#define TCB_SPAN_CONSTEXPR11 constexpr
+#else
+#define TCB_SPAN_CONSTEXPR11 TCB_SPAN_CONSTEXPR14
+#endif
+
+#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_deduction_guides)
+#define TCB_SPAN_HAVE_DEDUCTION_GUIDES
+#endif
+
+#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_lib_byte)
+#define TCB_SPAN_HAVE_STD_BYTE
+#endif
+
+#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_lib_array_constexpr)
+#define TCB_SPAN_HAVE_CONSTEXPR_STD_ARRAY_ETC
+#endif
+
+#if defined(TCB_SPAN_HAVE_CONSTEXPR_STD_ARRAY_ETC)
+#define TCB_SPAN_ARRAY_CONSTEXPR constexpr
+#else
+#define TCB_SPAN_ARRAY_CONSTEXPR
+#endif
+
+#ifdef TCB_SPAN_HAVE_STD_BYTE
+using byte = std::byte;
+#else
+using byte = unsigned char;
+#endif
+
+#if defined(TCB_SPAN_HAVE_CPP17)
+#define TCB_SPAN_NODISCARD [[nodiscard]]
+#else
+#define TCB_SPAN_NODISCARD
+#endif
+
+TCB_SPAN_INLINE_VAR constexpr std::size_t dynamic_extent = SIZE_MAX;
+
+template <typename ElementType, std::size_t Extent = dynamic_extent>
+class span;
+
+namespace detail {
+
+template <typename E, std::size_t S>
+struct span_storage {
+    constexpr span_storage() noexcept = default;
+
+    constexpr span_storage(E* p_ptr, std::size_t /*unused*/) noexcept
+       : ptr(p_ptr)
+    {}
+
+    E* ptr = nullptr;
+    static constexpr std::size_t size = S;
+};
+
+template <typename E>
+struct span_storage<E, dynamic_extent> {
+    constexpr span_storage() noexcept = default;
+
+    constexpr span_storage(E* p_ptr, std::size_t p_size) noexcept
+        : ptr(p_ptr), size(p_size)
+    {}
+
+    E* ptr = nullptr;
+    std::size_t size = 0;
+};
+
+// Reimplementation of C++17 std::size() and std::data()
+#if defined(TCB_SPAN_HAVE_CPP17) ||                                            \
+    defined(__cpp_lib_nonmember_container_access)
+using std::data;
+using std::size;
+#else
+template <class C>
+constexpr auto size(const C& c) -> decltype(c.size())
+{
+    return c.size();
+}
+
+template <class T, std::size_t N>
+constexpr std::size_t size(const T (&)[N]) noexcept
+{
+    return N;
+}
+
+template <class C>
+constexpr auto data(C& c) -> decltype(c.data())
+{
+    return c.data();
+}
+
+template <class C>
+constexpr auto data(const C& c) -> decltype(c.data())
+{
+    return c.data();
+}
+
+template <class T, std::size_t N>
+constexpr T* data(T (&array)[N]) noexcept
+{
+    return array;
+}
+
+template <class E>
+constexpr const E* data(std::initializer_list<E> il) noexcept
+{
+    return il.begin();
+}
+#endif // TCB_SPAN_HAVE_CPP17
+
+#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_lib_void_t)
+using std::void_t;
+#else
+template <typename...>
+using void_t = void;
+#endif
+
+template <typename T>
+using uncvref_t =
+    typename std::remove_cv<typename std::remove_reference<T>::type>::type;
+
+template <typename>
+struct is_span : std::false_type {};
+
+template <typename T, std::size_t S>
+struct is_span<span<T, S>> : std::true_type {};
+
+template <typename>
+struct is_std_array : std::false_type {};
+
+template <typename T, std::size_t N>
+struct is_std_array<std::array<T, N>> : std::true_type {};
+
+template <typename, typename = void>
+struct has_size_and_data : std::false_type {};
+
+template <typename T>
+struct has_size_and_data<T, void_t<decltype(detail::size(std::declval<T>())),
+                                   decltype(detail::data(std::declval<T>()))>>
+    : std::true_type {};
+
+template <typename C, typename U = uncvref_t<C>>
+struct is_container {
+    static constexpr bool value =
+        !is_span<U>::value && !is_std_array<U>::value &&
+        !std::is_array<U>::value && has_size_and_data<C>::value;
+};
+
+template <typename T>
+using remove_pointer_t = typename std::remove_pointer<T>::type;
+
+template <typename, typename, typename = void>
+struct is_container_element_type_compatible : std::false_type {};
+
+template <typename T, typename E>
+struct is_container_element_type_compatible<
+    T, E,
+    typename std::enable_if<
+        !std::is_same<typename std::remove_cv<decltype(
+                          detail::data(std::declval<T>()))>::type,
+                      void>::value>::type>
+    : std::is_convertible<
+          remove_pointer_t<decltype(detail::data(std::declval<T>()))> (*)[],
+          E (*)[]> {};
+
+template <typename, typename = std::size_t>
+struct is_complete : std::false_type {};
+
+template <typename T>
+struct is_complete<T, decltype(sizeof(T))> : std::true_type {};
+
+} // namespace detail
+
+template <typename ElementType, std::size_t Extent>
+class span {
+    static_assert(std::is_object<ElementType>::value,
+                  "A span's ElementType must be an object type (not a "
+                  "reference type or void)");
+    static_assert(detail::is_complete<ElementType>::value,
+                  "A span's ElementType must be a complete type (not a forward "
+                  "declaration)");
+    static_assert(!std::is_abstract<ElementType>::value,
+                  "A span's ElementType cannot be an abstract class type");
+
+    using storage_type = detail::span_storage<ElementType, Extent>;
+
+public:
+    // constants and types
+    using element_type = ElementType;
+    using value_type = typename std::remove_cv<ElementType>::type;
+    using size_type = std::size_t;
+    using difference_type = std::ptrdiff_t;
+    using pointer = element_type*;
+    using const_pointer = const element_type*;
+    using reference = element_type&;
+    using const_reference = const element_type&;
+    using iterator = pointer;
+    using reverse_iterator = std::reverse_iterator<iterator>;
+
+    static constexpr size_type extent = Extent;
+
+    // [span.cons], span constructors, copy, assignment, and destructor
+    template <
+        std::size_t E = Extent,
+        typename std::enable_if<(E == dynamic_extent || E <= 0), int>::type = 0>
+    constexpr span() noexcept // NOLINT : clang-tidy is mistaken. one cannot `default` a template ctor
+    {}
+
+    TCB_SPAN_CONSTEXPR11 span(pointer ptr, size_type count)
+        : storage_(ptr, count)
+    {
+        TCB_SPAN_EXPECT(extent == dynamic_extent || count == extent);
+    }
+
+    TCB_SPAN_CONSTEXPR11 span(pointer first_elem, pointer last_elem)
+        : storage_(first_elem, last_elem - first_elem)
+    {
+        TCB_SPAN_EXPECT(extent == dynamic_extent ||
+                        last_elem - first_elem ==
+                            static_cast<std::ptrdiff_t>(extent));
+    }
+
+    template <std::size_t N, std::size_t E = Extent,
+              typename std::enable_if<
+                  (E == dynamic_extent || N == E) &&
+                      detail::is_container_element_type_compatible<
+                          element_type (&)[N], ElementType>::value,
+                  int>::type = 0>
+    constexpr span(element_type (&arr)[N]) noexcept : storage_(arr, N)
+    {}
+
+    template <std::size_t N, std::size_t E = Extent,
+              typename std::enable_if<
+                  (E == dynamic_extent || N == E) &&
+                      detail::is_container_element_type_compatible<
+                          std::array<value_type, N>&, ElementType>::value,
+                  int>::type = 0>
+    TCB_SPAN_ARRAY_CONSTEXPR span(std::array<value_type, N>& arr) noexcept
+        : storage_(arr.data(), N)
+    {}
+
+    template <std::size_t N, std::size_t E = Extent,
+              typename std::enable_if<
+                  (E == dynamic_extent || N == E) &&
+                      detail::is_container_element_type_compatible<
+                          const std::array<value_type, N>&, ElementType>::value,
+                  int>::type = 0>
+    TCB_SPAN_ARRAY_CONSTEXPR span(const std::array<value_type, N>& arr) noexcept
+        : storage_(arr.data(), N)
+    {}
+
+    template <
+        typename Container, std::size_t E = Extent,
+        typename std::enable_if<
+            E == dynamic_extent && detail::is_container<Container>::value &&
+                detail::is_container_element_type_compatible<
+                    Container&, ElementType>::value,
+            int>::type = 0>
+    constexpr span(Container& cont)
+        : storage_(detail::data(cont), detail::size(cont))
+    {}
+
+    template <
+        typename Container, std::size_t E = Extent,
+        typename std::enable_if<
+            E == dynamic_extent && detail::is_container<Container>::value &&
+                detail::is_container_element_type_compatible<
+                    const Container&, ElementType>::value,
+            int>::type = 0>
+    constexpr span(const Container& cont)
+        : storage_(detail::data(cont), detail::size(cont))
+    {}
+
+    constexpr span(const span& other) noexcept = default;
+
+    template <typename OtherElementType, std::size_t OtherExtent,
+              typename std::enable_if<
+                  (Extent == OtherExtent || Extent == dynamic_extent) &&
+                      std::is_convertible<OtherElementType (*)[],
+                                          ElementType (*)[]>::value,
+                  int>::type = 0>
+    constexpr span(const span<OtherElementType, OtherExtent>& other) noexcept
+        : storage_(other.data(), other.size())
+    {}
+
+    ~span() noexcept = default;
+
+    TCB_SPAN_CONSTEXPR_ASSIGN span&
+    operator=(const span& other) noexcept = default;
+
+    // [span.sub], span subviews
+    template <std::size_t Count>
+    TCB_SPAN_CONSTEXPR11 span<element_type, Count> first() const
+    {
+        TCB_SPAN_EXPECT(Count <= size());
+        return {data(), Count};
+    }
+
+    template <std::size_t Count>
+    TCB_SPAN_CONSTEXPR11 span<element_type, Count> last() const
+    {
+        TCB_SPAN_EXPECT(Count <= size());
+        return {data() + (size() - Count), Count};
+    }
+
+    template <std::size_t Offset, std::size_t Count = dynamic_extent>
+    using subspan_return_t =
+        span<ElementType, Count != dynamic_extent
+                              ? Count
+                              : (Extent != dynamic_extent ? Extent - Offset
+                                                          : dynamic_extent)>;
+
+    template <std::size_t Offset, std::size_t Count = dynamic_extent>
+    TCB_SPAN_CONSTEXPR11 subspan_return_t<Offset, Count> subspan() const
+    {
+        TCB_SPAN_EXPECT(Offset <= size() &&
+                        (Count == dynamic_extent || Offset + Count <= size()));
+        return {data() + Offset,
+                Count != dynamic_extent ? Count : size() - Offset};
+    }
+
+    TCB_SPAN_CONSTEXPR11 span<element_type, dynamic_extent>
+    first(size_type count) const
+    {
+        TCB_SPAN_EXPECT(count <= size());
+        return {data(), count};
+    }
+
+    TCB_SPAN_CONSTEXPR11 span<element_type, dynamic_extent>
+    last(size_type count) const
+    {
+        TCB_SPAN_EXPECT(count <= size());
+        return {data() + (size() - count), count};
+    }
+
+    TCB_SPAN_CONSTEXPR11 span<element_type, dynamic_extent>
+    subspan(size_type offset, size_type count = dynamic_extent) const
+    {
+        TCB_SPAN_EXPECT(offset <= size() &&
+                        (count == dynamic_extent || offset + count <= size()));
+        return {data() + offset,
+                count == dynamic_extent ? size() - offset : count};
+    }
+
+    // [span.obs], span observers
+    constexpr size_type size() const noexcept { return storage_.size; }
+
+    constexpr size_type size_bytes() const noexcept
+    {
+        return size() * sizeof(element_type);
+    }
+
+    TCB_SPAN_NODISCARD constexpr bool empty() const noexcept
+    {
+        return size() == 0;
+    }
+
+    // [span.elem], span element access
+    TCB_SPAN_CONSTEXPR11 reference operator[](size_type idx) const
+    {
+        TCB_SPAN_EXPECT(idx < size());
+        return *(data() + idx);
+    }
+
+    TCB_SPAN_CONSTEXPR11 reference front() const
+    {
+        TCB_SPAN_EXPECT(!empty());
+        return *data();
+    }
+
+    TCB_SPAN_CONSTEXPR11 reference back() const
+    {
+        TCB_SPAN_EXPECT(!empty());
+        return *(data() + (size() - 1));
+    }
+
+    constexpr pointer data() const noexcept { return storage_.ptr; }
+
+    // [span.iterators], span iterator support
+    constexpr iterator begin() const noexcept { return data(); }
+
+    constexpr iterator end() const noexcept { return data() + size(); }
+
+    TCB_SPAN_ARRAY_CONSTEXPR reverse_iterator rbegin() const noexcept
+    {
+        return reverse_iterator(end());
+    }
+
+    TCB_SPAN_ARRAY_CONSTEXPR reverse_iterator rend() const noexcept
+    {
+        return reverse_iterator(begin());
+    }
+
+private:
+    storage_type storage_{};
+};
+
+#ifdef TCB_SPAN_HAVE_DEDUCTION_GUIDES
+
+/* Deduction Guides */
+template <class T, std::size_t N>
+span(T (&)[N])->span<T, N>;
+
+template <class T, std::size_t N>
+span(std::array<T, N>&)->span<T, N>;
+
+template <class T, std::size_t N>
+span(const std::array<T, N>&)->span<const T, N>;
+
+template <class Container>
+span(Container&)->span<typename Container::value_type>;
+
+template <class Container>
+span(const Container&)->span<const typename Container::value_type>;
+
+#endif // TCB_HAVE_DEDUCTION_GUIDES
+
+template <typename ElementType, std::size_t Extent>
+constexpr span<ElementType, Extent>
+make_span(span<ElementType, Extent> s) noexcept
+{
+    return s;
+}
+
+template <typename T, std::size_t N>
+constexpr span<T, N> make_span(T (&arr)[N]) noexcept
+{
+    return {arr};
+}
+
+template <typename T, std::size_t N>
+TCB_SPAN_ARRAY_CONSTEXPR span<T, N> make_span(std::array<T, N>& arr) noexcept
+{
+    return {arr};
+}
+
+template <typename T, std::size_t N>
+TCB_SPAN_ARRAY_CONSTEXPR span<const T, N>
+make_span(const std::array<T, N>& arr) noexcept
+{
+    return {arr};
+}
+
+template <typename Container>
+constexpr span<typename Container::value_type> make_span(Container& cont)
+{
+    return {cont};
+}
+
+template <typename Container>
+constexpr span<const typename Container::value_type>
+make_span(const Container& cont)
+{
+    return {cont};
+}
+
+template <typename ElementType, std::size_t Extent>
+span<const byte, ((Extent == dynamic_extent) ? dynamic_extent
+                                             : sizeof(ElementType) * Extent)>
+as_bytes(span<ElementType, Extent> s) noexcept
+{
+    return {reinterpret_cast<const byte*>(s.data()), s.size_bytes()};
+}
+
+template <
+    class ElementType, std::size_t Extent,
+    typename std::enable_if<!std::is_const<ElementType>::value, int>::type = 0>
+span<byte, ((Extent == dynamic_extent) ? dynamic_extent
+                                       : sizeof(ElementType) * Extent)>
+as_writable_bytes(span<ElementType, Extent> s) noexcept
+{
+    return {reinterpret_cast<byte*>(s.data()), s.size_bytes()};
+}
+
+template <std::size_t N, typename E, std::size_t S>
+constexpr auto get(span<E, S> s) -> decltype(s[N])
+{
+    return s[N];
+}
+
+} // namespace TCB_SPAN_NAMESPACE_NAME
+
+namespace std {
+
+// see:     https://gcc.gnu.org/bugzilla/show_bug.cgi?id=82716
+// libc++:  https://reviews.llvm.org/D55466#1325498
+//          `libc++` changed to use `struct`
+// spec:    http://eel.is/c++draft/tuple.helper
+//          Spec says to use `struct`.
+// MSVC:    Has different ABI for `class`/`struct`.
+//          Defined `tuple_size` as `class`.
+#if defined(_MSC_VER)
+    #define TCB_SPAN_TUPLE_SIZE_KIND class
+#else
+    #define TCB_SPAN_TUPLE_SIZE_KIND struct
+#endif
+
+#if defined(__clang__)
+    #pragma clang diagnostic push
+    #pragma clang diagnostic ignored "-Wmismatched-tags"
+#endif
+
+template <typename ElementType, std::size_t Extent>
+TCB_SPAN_TUPLE_SIZE_KIND tuple_size<TCB_SPAN_NAMESPACE_NAME::span<ElementType, Extent>>
+    : public integral_constant<std::size_t, Extent> {};
+
+template <typename ElementType>
+TCB_SPAN_TUPLE_SIZE_KIND tuple_size<TCB_SPAN_NAMESPACE_NAME::span<
+    ElementType, TCB_SPAN_NAMESPACE_NAME::dynamic_extent>>; // not defined
+
+template <std::size_t I, typename ElementType, std::size_t Extent>
+TCB_SPAN_TUPLE_SIZE_KIND tuple_element<I, TCB_SPAN_NAMESPACE_NAME::span<ElementType, Extent>> {
+public:
+    static_assert(Extent != TCB_SPAN_NAMESPACE_NAME::dynamic_extent &&
+                      I < Extent,
+                  "");
+    using type = ElementType;
+};
+
+#if defined(__clang__)
+    #pragma clang diagnostic pop
+#endif
+
+#undef TCB_SPAN_TUPLE_SIZE_KIND
+
+} // end namespace std
+
+#endif // TCB_SPAN_HPP_INCLUDED
+
+// clang-format on
+
+namespace souffle {
+constexpr auto dynamic_extent = tcb::dynamic_extent;
+
+template <typename A, std::size_t E = tcb::dynamic_extent>
+using span = tcb::span<A, E>;
+}  // namespace souffle
+
+#endif
diff --git a/cbits/souffle/utility/tinyformat.h b/cbits/souffle/utility/tinyformat.h
--- a/cbits/souffle/utility/tinyformat.h
+++ b/cbits/souffle/utility/tinyformat.h
@@ -50,7 +50,7 @@
 //
 //   std::string weekday = "Wednesday";
 //   const char* month = "July";
-//   size_t day = 27;
+//   std::size_t day = 27;
 //   long hour = 14;
 //   int min = 44;
 //
@@ -66,7 +66,7 @@
 //
 // The strange types here emphasize the type safety of the interface; it is
 // possible to print a std::string using the "%s" conversion, and a
-// size_t using the "%d" conversion.  A similar result could be achieved
+// std::size_t using the "%d" conversion.  A similar result could be achieved
 // using either of the tfm::format() functions.  One prints on a user provided
 // stream:
 //
diff --git a/lib/Language/Souffle/Compiled.hs b/lib/Language/Souffle/Compiled.hs
--- a/lib/Language/Souffle/Compiled.hs
+++ b/lib/Language/Souffle/Compiled.hs
@@ -29,7 +29,6 @@
   ) where
 
 import Prelude hiding ( init )
-import Control.Exception
 import Control.Monad.Except
 import Control.Monad.State.Strict
 import Data.Foldable ( traverse_ )
@@ -172,11 +171,10 @@
       else do
         ptr <- gets castPtr
         bs <- liftIO $ BSU.unsafePackCStringLen (ptr, fromIntegral byteCount)
-        -- NOTE: `evaluate` is needed here to force the text value. A copy needs to
-        -- be made (using toShort), before the bytearray is overwritten.
-        bss <- liftIO $ evaluate $ BSS.toShort bs
         put $ ptr `plusPtr` fromIntegral byteCount
-        pure $ TSU.fromShortByteStringUnsafe bss
+        -- NOTE: $! is needed here to force the text value. A copy needs to
+        -- be made (using toShort), before the bytearray is overwritten.
+        pure $! TSU.fromShortByteStringUnsafe $ BSS.toShort bs
   {-# INLINABLE popText #-}
 
 
diff --git a/souffle-haskell.cabal b/souffle-haskell.cabal
--- a/souffle-haskell.cabal
+++ b/souffle-haskell.cabal
@@ -3,11 +3,9 @@
 -- This file has been generated from package.yaml by hpack version 0.34.2.
 --
 -- see: https://github.com/sol/hpack
---
--- hash: 7e179d1e2ce8ddd1c9a53631a6b4743b754065bf70a2130d654d738b83cec53a
 
 name:           souffle-haskell
-version:        3.0.0
+version:        3.1.0
 synopsis:       Souffle Datalog bindings for Haskell
 description:    Souffle Datalog bindings for Haskell.
 category:       Logic Programming, Foreign Binding, Bindings
@@ -25,9 +23,10 @@
     LICENSE
     cbits/souffle.h
     cbits/souffle/CompiledSouffle.h
-    cbits/souffle/CompiledTuple.h
     cbits/souffle/datastructure/Brie.h
     cbits/souffle/datastructure/BTree.h
+    cbits/souffle/datastructure/ConcurrentFlyweight.h
+    cbits/souffle/datastructure/ConcurrentInsertOnlyHashMap.h
     cbits/souffle/datastructure/EquivalenceRelation.h
     cbits/souffle/datastructure/LambdaBTree.h
     cbits/souffle/datastructure/PiggyList.h
@@ -54,12 +53,15 @@
     cbits/souffle/utility/EvaluatorUtil.h
     cbits/souffle/utility/FileUtil.h
     cbits/souffle/utility/FunctionalUtil.h
+    cbits/souffle/utility/Iteration.h
     cbits/souffle/utility/json11.h
     cbits/souffle/utility/MiscUtil.h
     cbits/souffle/utility/ParallelUtil.h
+    cbits/souffle/utility/span.h
     cbits/souffle/utility/StreamUtil.h
     cbits/souffle/utility/StringUtil.h
     cbits/souffle/utility/tinyformat.h
+    cbits/souffle/utility/Types.h
     cbits/souffle.cpp
     cbits/souffle/LICENSE
 
@@ -90,15 +92,19 @@
       cbits/souffle
   install-includes:
       souffle/CompiledSouffle.h
-      souffle/CompiledTuple.h
       souffle/RamTypes.h
       souffle/RecordTable.h
+      souffle/datastructure/ConcurrentFlyweight.h
+      souffle/datastructure/ConcurrentInsertOnlyHashMap.h
+      souffle/utility/ParallelUtil.h
+      souffle/utility/span.h
       souffle/SignalHandler.h
       souffle/SouffleInterface.h
       souffle/SymbolTable.h
       souffle/utility/MiscUtil.h
+      souffle/utility/Iteration.h
+      souffle/utility/Types.h
       souffle/utility/tinyformat.h
-      souffle/utility/ParallelUtil.h
       souffle/utility/StreamUtil.h
       souffle/utility/ContainerUtil.h
       souffle/datastructure/Brie.h
@@ -112,8 +118,8 @@
       souffle/io/IOSystem.h
       souffle/io/ReadStream.h
       souffle/io/SerialisationStream.h
-      souffle/utility/json11.h
       souffle/utility/StringUtil.h
+      souffle/utility/json11.h
       souffle/io/ReadStreamCSV.h
       souffle/utility/FileUtil.h
       souffle/io/gzfstream.h
@@ -166,15 +172,19 @@
       cbits/souffle
   install-includes:
       souffle/CompiledSouffle.h
-      souffle/CompiledTuple.h
       souffle/RamTypes.h
       souffle/RecordTable.h
+      souffle/datastructure/ConcurrentFlyweight.h
+      souffle/datastructure/ConcurrentInsertOnlyHashMap.h
+      souffle/utility/ParallelUtil.h
+      souffle/utility/span.h
       souffle/SignalHandler.h
       souffle/SouffleInterface.h
       souffle/SymbolTable.h
       souffle/utility/MiscUtil.h
+      souffle/utility/Iteration.h
+      souffle/utility/Types.h
       souffle/utility/tinyformat.h
-      souffle/utility/ParallelUtil.h
       souffle/utility/StreamUtil.h
       souffle/utility/ContainerUtil.h
       souffle/datastructure/Brie.h
@@ -188,8 +198,8 @@
       souffle/io/IOSystem.h
       souffle/io/ReadStream.h
       souffle/io/SerialisationStream.h
-      souffle/utility/json11.h
       souffle/utility/StringUtil.h
+      souffle/utility/json11.h
       souffle/io/ReadStreamCSV.h
       souffle/utility/FileUtil.h
       souffle/io/gzfstream.h
@@ -246,15 +256,19 @@
       cbits/souffle
   install-includes:
       souffle/CompiledSouffle.h
-      souffle/CompiledTuple.h
       souffle/RamTypes.h
       souffle/RecordTable.h
+      souffle/datastructure/ConcurrentFlyweight.h
+      souffle/datastructure/ConcurrentInsertOnlyHashMap.h
+      souffle/utility/ParallelUtil.h
+      souffle/utility/span.h
       souffle/SignalHandler.h
       souffle/SouffleInterface.h
       souffle/SymbolTable.h
       souffle/utility/MiscUtil.h
+      souffle/utility/Iteration.h
+      souffle/utility/Types.h
       souffle/utility/tinyformat.h
-      souffle/utility/ParallelUtil.h
       souffle/utility/StreamUtil.h
       souffle/utility/ContainerUtil.h
       souffle/datastructure/Brie.h
@@ -268,8 +282,8 @@
       souffle/io/IOSystem.h
       souffle/io/ReadStream.h
       souffle/io/SerialisationStream.h
-      souffle/utility/json11.h
       souffle/utility/StringUtil.h
+      souffle/utility/json11.h
       souffle/io/ReadStreamCSV.h
       souffle/utility/FileUtil.h
       souffle/io/gzfstream.h
diff --git a/tests/fixtures/edge_cases.cpp b/tests/fixtures/edge_cases.cpp
--- a/tests/fixtures/edge_cases.cpp
+++ b/tests/fixtures/edge_cases.cpp
@@ -7,6 +7,7 @@
 namespace souffle {
 static const RamDomain RAM_BIT_SHIFT_MASK = RAM_DOMAIN_SIZE - 1;
 struct t_btree_iii__0_1_2__111 {
+static constexpr Relation::arity_type Arity = 3;
 using t_tuple = Tuple<RamDomain, 3>;
 struct t_comparator_0{
  int operator()(const t_tuple& a, const t_tuple& b) const {
@@ -109,6 +110,7 @@
 }
 };
 struct t_btree_i__0__1 {
+static constexpr Relation::arity_type Arity = 1;
 using t_tuple = Tuple<RamDomain, 1>;
 struct t_comparator_0{
  int operator()(const t_tuple& a, const t_tuple& b) const {
@@ -211,6 +213,7 @@
 }
 };
 struct t_btree_uif__0_1_2__111 {
+static constexpr Relation::arity_type Arity = 3;
 using t_tuple = Tuple<RamDomain, 3>;
 struct t_comparator_0{
  int operator()(const t_tuple& a, const t_tuple& b) const {
@@ -323,7 +326,7 @@
    return result;
 }
 private:
-static inline std::string substr_wrapper(const std::string& str, size_t idx, size_t len) {
+static inline std::string substr_wrapper(const std::string& str, std::size_t idx, std::size_t len) {
    std::string result; 
    try { result = str.substr(idx,len); } catch(...) { 
      std::cerr << "warning: wrong index position provided by substr(\"";
@@ -342,48 +345,53 @@
 RecordTable recordTable;
 // -- Table: empty_strings
 Own<t_btree_iii__0_1_2__111> rel_1_empty_strings = mk<t_btree_iii__0_1_2__111>();
-souffle::RelationWrapper<0,t_btree_iii__0_1_2__111,Tuple<RamDomain,3>,3,0> wrapper_rel_1_empty_strings;
+souffle::RelationWrapper<t_btree_iii__0_1_2__111> wrapper_rel_1_empty_strings;
 // -- Table: long_strings
 Own<t_btree_i__0__1> rel_2_long_strings = mk<t_btree_i__0__1>();
-souffle::RelationWrapper<1,t_btree_i__0__1,Tuple<RamDomain,1>,1,0> wrapper_rel_2_long_strings;
-// -- Table: no_strings
-Own<t_btree_uif__0_1_2__111> rel_3_no_strings = mk<t_btree_uif__0_1_2__111>();
-souffle::RelationWrapper<2,t_btree_uif__0_1_2__111,Tuple<RamDomain,3>,3,0> wrapper_rel_3_no_strings;
+souffle::RelationWrapper<t_btree_i__0__1> wrapper_rel_2_long_strings;
 // -- Table: unicode
-Own<t_btree_i__0__1> rel_4_unicode = mk<t_btree_i__0__1>();
-souffle::RelationWrapper<3,t_btree_i__0__1,Tuple<RamDomain,1>,1,0> wrapper_rel_4_unicode;
+Own<t_btree_i__0__1> rel_3_unicode = mk<t_btree_i__0__1>();
+souffle::RelationWrapper<t_btree_i__0__1> wrapper_rel_3_unicode;
+// -- Table: no_strings
+Own<t_btree_uif__0_1_2__111> rel_4_no_strings = mk<t_btree_uif__0_1_2__111>();
+souffle::RelationWrapper<t_btree_uif__0_1_2__111> wrapper_rel_4_no_strings;
 public:
-Sf_edge_cases() : 
-wrapper_rel_1_empty_strings(*rel_1_empty_strings,symTable,"empty_strings",std::array<const char *,3>{{"s:symbol","s:symbol","i:number"}},std::array<const char *,3>{{"s","s2","n"}}),
-
-wrapper_rel_2_long_strings(*rel_2_long_strings,symTable,"long_strings",std::array<const char *,1>{{"s:symbol"}},std::array<const char *,1>{{"s"}}),
-
-wrapper_rel_3_no_strings(*rel_3_no_strings,symTable,"no_strings",std::array<const char *,3>{{"u:unsigned","i:number","f:float"}},std::array<const char *,3>{{"u","n","f"}}),
-
-wrapper_rel_4_unicode(*rel_4_unicode,symTable,"unicode",std::array<const char *,1>{{"s:symbol"}},std::array<const char *,1>{{"s"}}){
-addRelation("empty_strings",&wrapper_rel_1_empty_strings,true,true);
-addRelation("long_strings",&wrapper_rel_2_long_strings,true,true);
-addRelation("no_strings",&wrapper_rel_3_no_strings,true,true);
-addRelation("unicode",&wrapper_rel_4_unicode,true,true);
+Sf_edge_cases()
+: wrapper_rel_1_empty_strings(0, *rel_1_empty_strings, *this, "empty_strings", std::array<const char *,3>{{"s:symbol","s:symbol","i:number"}}, std::array<const char *,3>{{"s","s2","n"}}, 0)
+, wrapper_rel_2_long_strings(1, *rel_2_long_strings, *this, "long_strings", std::array<const char *,1>{{"s:symbol"}}, std::array<const char *,1>{{"s"}}, 0)
+, wrapper_rel_3_unicode(2, *rel_3_unicode, *this, "unicode", std::array<const char *,1>{{"s:symbol"}}, std::array<const char *,1>{{"s"}}, 0)
+, wrapper_rel_4_no_strings(3, *rel_4_no_strings, *this, "no_strings", std::array<const char *,3>{{"u:unsigned","i:number","f:float"}}, std::array<const char *,3>{{"u","n","f"}}, 0)
+{
+addRelation("empty_strings", wrapper_rel_1_empty_strings, true, true);
+addRelation("long_strings", wrapper_rel_2_long_strings, true, true);
+addRelation("unicode", wrapper_rel_3_unicode, true, true);
+addRelation("no_strings", wrapper_rel_4_no_strings, true, true);
 }
 ~Sf_edge_cases() {
 }
+
 private:
-std::string inputDirectory;
-std::string outputDirectory;
-bool performIO;
-std::atomic<RamDomain> ctr{};
+std::string             inputDirectory;
+std::string             outputDirectory;
+SignalHandler*          signalHandler {SignalHandler::instance()};
+std::atomic<RamDomain>  ctr {};
+std::atomic<std::size_t>     iter {};
+bool                    performIO = false;
 
-std::atomic<size_t> iter{};
-void runFunction(std::string inputDirectoryArg = "", std::string outputDirectoryArg = "", bool performIOArg = false) {
-this->inputDirectory = inputDirectoryArg;
-this->outputDirectory = outputDirectoryArg;
-this->performIO = performIOArg;
-SignalHandler::instance()->set();
+void runFunction(std::string  inputDirectoryArg   = "",
+                 std::string  outputDirectoryArg  = "",
+                 bool         performIOArg        = false) {
+    this->inputDirectory  = std::move(inputDirectoryArg);
+    this->outputDirectory = std::move(outputDirectoryArg);
+    this->performIO       = performIOArg;
+
+    // set default threads (in embedded mode)
+    // if this is not set, and omp is used, the default omp setting of number of cores is used.
 #if defined(_OPENMP)
-if (getNumThreads() > 0) {omp_set_num_threads(getNumThreads());}
+    if (0 < getNumThreads()) { omp_set_num_threads(getNumThreads()); }
 #endif
 
+    signalHandler->set();
 // -- query evaluation --
 {
  std::vector<RamDomain> args, ret;
@@ -403,7 +411,7 @@
 }
 
 // -- relation hint statistics --
-SignalHandler::instance()->reset();
+signalHandler->reset();
 }
 public:
 void run() override { runFunction("", "", false); }
@@ -412,40 +420,40 @@
 }
 public:
 void printAll(std::string outputDirectoryArg = "") override {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s"},{"name","unicode"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"s\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"s:symbol\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s\ts2\tn"},{"auxArity","0"},{"name","empty_strings"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 3, \"params\": [\"s\", \"s2\", \"n\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 3, \"types\": [\"s:symbol\", \"s:symbol\", \"i:number\"]}}"}});
 if (!outputDirectoryArg.empty()) {directiveMap["output-dir"] = outputDirectoryArg;}
-IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_4_unicode);
+IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_1_empty_strings);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s"},{"name","long_strings"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"s\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"s:symbol\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s"},{"auxArity","0"},{"name","long_strings"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"s\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"s:symbol\"]}}"}});
 if (!outputDirectoryArg.empty()) {directiveMap["output-dir"] = outputDirectoryArg;}
 IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_2_long_strings);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","u\tn\tf"},{"name","no_strings"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 3, \"auxArity\": 0, \"params\": [\"u\", \"n\", \"f\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 3, \"auxArity\": 0, \"types\": [\"u:unsigned\", \"i:number\", \"f:float\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s"},{"auxArity","0"},{"name","unicode"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"s\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"s:symbol\"]}}"}});
 if (!outputDirectoryArg.empty()) {directiveMap["output-dir"] = outputDirectoryArg;}
-IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_3_no_strings);
+IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_3_unicode);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s\ts2\tn"},{"name","empty_strings"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 3, \"auxArity\": 0, \"params\": [\"s\", \"s2\", \"n\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 3, \"auxArity\": 0, \"types\": [\"s:symbol\", \"s:symbol\", \"i:number\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","u\tn\tf"},{"auxArity","0"},{"name","no_strings"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 3, \"params\": [\"u\", \"n\", \"f\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 3, \"types\": [\"u:unsigned\", \"i:number\", \"f:float\"]}}"}});
 if (!outputDirectoryArg.empty()) {directiveMap["output-dir"] = outputDirectoryArg;}
-IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_1_empty_strings);
+IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_4_no_strings);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 }
 public:
 void loadAll(std::string inputDirectoryArg = "") override {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s"},{"fact-dir","."},{"name","long_strings"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"s\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"s:symbol\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s"},{"auxArity","0"},{"fact-dir","."},{"name","long_strings"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"s\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"s:symbol\"]}}"}});
 if (!inputDirectoryArg.empty()) {directiveMap["fact-dir"] = inputDirectoryArg;}
 IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_2_long_strings);
 } catch (std::exception& e) {std::cerr << "Error loading data: " << e.what() << '\n';}
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s"},{"fact-dir","."},{"name","unicode"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"s\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"s:symbol\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s\ts2\tn"},{"auxArity","0"},{"fact-dir","."},{"name","empty_strings"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 3, \"params\": [\"s\", \"s2\", \"n\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 3, \"types\": [\"s:symbol\", \"s:symbol\", \"i:number\"]}}"}});
 if (!inputDirectoryArg.empty()) {directiveMap["fact-dir"] = inputDirectoryArg;}
-IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_4_unicode);
+IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_1_empty_strings);
 } catch (std::exception& e) {std::cerr << "Error loading data: " << e.what() << '\n';}
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","u\tn\tf"},{"fact-dir","."},{"name","no_strings"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 3, \"auxArity\": 0, \"params\": [\"u\", \"n\", \"f\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 3, \"auxArity\": 0, \"types\": [\"u:unsigned\", \"i:number\", \"f:float\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s"},{"auxArity","0"},{"fact-dir","."},{"name","unicode"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"s\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"s:symbol\"]}}"}});
 if (!inputDirectoryArg.empty()) {directiveMap["fact-dir"] = inputDirectoryArg;}
-IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_3_no_strings);
+IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_3_unicode);
 } catch (std::exception& e) {std::cerr << "Error loading data: " << e.what() << '\n';}
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s\ts2\tn"},{"fact-dir","."},{"name","empty_strings"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 3, \"auxArity\": 0, \"params\": [\"s\", \"s2\", \"n\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 3, \"auxArity\": 0, \"types\": [\"s:symbol\", \"s:symbol\", \"i:number\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","u\tn\tf"},{"auxArity","0"},{"fact-dir","."},{"name","no_strings"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 3, \"params\": [\"u\", \"n\", \"f\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 3, \"types\": [\"u:unsigned\", \"i:number\", \"f:float\"]}}"}});
 if (!inputDirectoryArg.empty()) {directiveMap["fact-dir"] = inputDirectoryArg;}
-IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_1_empty_strings);
+IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_4_no_strings);
 } catch (std::exception& e) {std::cerr << "Error loading data: " << e.what() << '\n';}
 }
 public:
@@ -458,54 +466,62 @@
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 try {std::map<std::string, std::string> rwOperation;
 rwOperation["IO"] = "stdout";
+rwOperation["name"] = "empty_strings";
+rwOperation["types"] = "{\"relation\": {\"arity\": 3, \"auxArity\": 0, \"types\": [\"s:symbol\", \"s:symbol\", \"i:number\"]}}";
+IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_1_empty_strings);
+} catch (std::exception& e) {std::cerr << e.what();exit(1);}
+try {std::map<std::string, std::string> rwOperation;
+rwOperation["IO"] = "stdout";
 rwOperation["name"] = "unicode";
 rwOperation["types"] = "{\"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"s:symbol\"]}}";
-IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_4_unicode);
+IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_3_unicode);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 try {std::map<std::string, std::string> rwOperation;
 rwOperation["IO"] = "stdout";
 rwOperation["name"] = "no_strings";
 rwOperation["types"] = "{\"relation\": {\"arity\": 3, \"auxArity\": 0, \"types\": [\"u:unsigned\", \"i:number\", \"f:float\"]}}";
-IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_3_no_strings);
+IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_4_no_strings);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
+}
+public:
+void dumpOutputs() override {
 try {std::map<std::string, std::string> rwOperation;
 rwOperation["IO"] = "stdout";
 rwOperation["name"] = "empty_strings";
 rwOperation["types"] = "{\"relation\": {\"arity\": 3, \"auxArity\": 0, \"types\": [\"s:symbol\", \"s:symbol\", \"i:number\"]}}";
 IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_1_empty_strings);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
-}
-public:
-void dumpOutputs() override {
 try {std::map<std::string, std::string> rwOperation;
 rwOperation["IO"] = "stdout";
-rwOperation["name"] = "unicode";
+rwOperation["name"] = "long_strings";
 rwOperation["types"] = "{\"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"s:symbol\"]}}";
-IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_4_unicode);
+IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_2_long_strings);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 try {std::map<std::string, std::string> rwOperation;
 rwOperation["IO"] = "stdout";
-rwOperation["name"] = "long_strings";
+rwOperation["name"] = "unicode";
 rwOperation["types"] = "{\"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"s:symbol\"]}}";
-IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_2_long_strings);
+IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_3_unicode);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 try {std::map<std::string, std::string> rwOperation;
 rwOperation["IO"] = "stdout";
 rwOperation["name"] = "no_strings";
 rwOperation["types"] = "{\"relation\": {\"arity\": 3, \"auxArity\": 0, \"types\": [\"u:unsigned\", \"i:number\", \"f:float\"]}}";
-IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_3_no_strings);
-} catch (std::exception& e) {std::cerr << e.what();exit(1);}
-try {std::map<std::string, std::string> rwOperation;
-rwOperation["IO"] = "stdout";
-rwOperation["name"] = "empty_strings";
-rwOperation["types"] = "{\"relation\": {\"arity\": 3, \"auxArity\": 0, \"types\": [\"s:symbol\", \"s:symbol\", \"i:number\"]}}";
-IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_1_empty_strings);
+IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_4_no_strings);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 }
 public:
 SymbolTable& getSymbolTable() override {
 return symTable;
 }
+RecordTable& getRecordTable() override {
+return recordTable;
+}
+void setNumThreads(std::size_t numThreadsValue) override {
+SouffleProgram::setNumThreads(numThreadsValue);
+symTable.setNumLanes(getNumThreads());
+recordTable.setNumLanes(getNumThreads());
+}
 void executeSubroutine(std::string name, const std::vector<RamDomain>& args, std::vector<RamDomain>& ret) override {
 if (name == "stratum_0") {
 subroutine_0(args, ret);
@@ -526,34 +542,34 @@
 #endif // _MSC_VER
 void subroutine_0(const std::vector<RamDomain>& args, std::vector<RamDomain>& ret) {
 if (performIO) {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s\ts2\tn"},{"fact-dir","."},{"name","empty_strings"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 3, \"auxArity\": 0, \"params\": [\"s\", \"s2\", \"n\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 3, \"auxArity\": 0, \"types\": [\"s:symbol\", \"s:symbol\", \"i:number\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s\ts2\tn"},{"auxArity","0"},{"fact-dir","."},{"name","empty_strings"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 3, \"params\": [\"s\", \"s2\", \"n\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 3, \"types\": [\"s:symbol\", \"s:symbol\", \"i:number\"]}}"}});
 if (!inputDirectory.empty()) {directiveMap["fact-dir"] = inputDirectory;}
 IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_1_empty_strings);
 } catch (std::exception& e) {std::cerr << "Error loading data: " << e.what() << '\n';}
 }
-SignalHandler::instance()->setMsg(R"_(empty_strings("","",42).
-in file /home/luc/souffle-haskell/tests/fixtures/edge_cases.dl [20:1-20:27])_");
+signalHandler->setMsg(R"_(empty_strings("","",42).
+in file /home/luc/personal/souffle-haskell/tests/fixtures/edge_cases.dl [20:1-20:27])_");
 [&](){
 CREATE_OP_CONTEXT(rel_1_empty_strings_op_ctxt,rel_1_empty_strings->createContext());
 Tuple<RamDomain,3> tuple{{ramBitCast(RamSigned(0)),ramBitCast(RamSigned(0)),ramBitCast(RamSigned(42))}};
 rel_1_empty_strings->insert(tuple,READ_OP_CONTEXT(rel_1_empty_strings_op_ctxt));
 }
-();SignalHandler::instance()->setMsg(R"_(empty_strings("","abc",42).
-in file /home/luc/souffle-haskell/tests/fixtures/edge_cases.dl [21:1-21:30])_");
+();signalHandler->setMsg(R"_(empty_strings("","abc",42).
+in file /home/luc/personal/souffle-haskell/tests/fixtures/edge_cases.dl [21:1-21:30])_");
 [&](){
 CREATE_OP_CONTEXT(rel_1_empty_strings_op_ctxt,rel_1_empty_strings->createContext());
 Tuple<RamDomain,3> tuple{{ramBitCast(RamSigned(0)),ramBitCast(RamSigned(1)),ramBitCast(RamSigned(42))}};
 rel_1_empty_strings->insert(tuple,READ_OP_CONTEXT(rel_1_empty_strings_op_ctxt));
 }
-();SignalHandler::instance()->setMsg(R"_(empty_strings("abc","",42).
-in file /home/luc/souffle-haskell/tests/fixtures/edge_cases.dl [22:1-22:30])_");
+();signalHandler->setMsg(R"_(empty_strings("abc","",42).
+in file /home/luc/personal/souffle-haskell/tests/fixtures/edge_cases.dl [22:1-22:30])_");
 [&](){
 CREATE_OP_CONTEXT(rel_1_empty_strings_op_ctxt,rel_1_empty_strings->createContext());
 Tuple<RamDomain,3> tuple{{ramBitCast(RamSigned(1)),ramBitCast(RamSigned(0)),ramBitCast(RamSigned(42))}};
 rel_1_empty_strings->insert(tuple,READ_OP_CONTEXT(rel_1_empty_strings_op_ctxt));
 }
 ();if (performIO) {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s\ts2\tn"},{"name","empty_strings"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 3, \"auxArity\": 0, \"params\": [\"s\", \"s2\", \"n\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 3, \"auxArity\": 0, \"types\": [\"s:symbol\", \"s:symbol\", \"i:number\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s\ts2\tn"},{"auxArity","0"},{"name","empty_strings"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 3, \"params\": [\"s\", \"s2\", \"n\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 3, \"types\": [\"s:symbol\", \"s:symbol\", \"i:number\"]}}"}});
 if (!outputDirectory.empty()) {directiveMap["output-dir"] = outputDirectory;}
 IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_1_empty_strings);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
@@ -567,20 +583,20 @@
 #endif // _MSC_VER
 void subroutine_1(const std::vector<RamDomain>& args, std::vector<RamDomain>& ret) {
 if (performIO) {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s"},{"fact-dir","."},{"name","long_strings"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"s\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"s:symbol\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s"},{"auxArity","0"},{"fact-dir","."},{"name","long_strings"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"s\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"s:symbol\"]}}"}});
 if (!inputDirectory.empty()) {directiveMap["fact-dir"] = inputDirectory;}
 IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_2_long_strings);
 } catch (std::exception& e) {std::cerr << "Error loading data: " << e.what() << '\n';}
 }
-SignalHandler::instance()->setMsg(R"_(long_strings("long_string_from_DL:...............................................................................................................................................................................................................................................................................................end").
-in file /home/luc/souffle-haskell/tests/fixtures/edge_cases.dl [25:1-25:328])_");
+signalHandler->setMsg(R"_(long_strings("long_string_from_DL:...............................................................................................................................................................................................................................................................................................end").
+in file /home/luc/personal/souffle-haskell/tests/fixtures/edge_cases.dl [25:1-25:328])_");
 [&](){
 CREATE_OP_CONTEXT(rel_2_long_strings_op_ctxt,rel_2_long_strings->createContext());
 Tuple<RamDomain,1> tuple{{ramBitCast(RamSigned(2))}};
 rel_2_long_strings->insert(tuple,READ_OP_CONTEXT(rel_2_long_strings_op_ctxt));
 }
 ();if (performIO) {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s"},{"name","long_strings"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"s\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"s:symbol\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s"},{"auxArity","0"},{"name","long_strings"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"s\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"s:symbol\"]}}"}});
 if (!outputDirectory.empty()) {directiveMap["output-dir"] = outputDirectory;}
 IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_2_long_strings);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
@@ -594,29 +610,29 @@
 #endif // _MSC_VER
 void subroutine_2(const std::vector<RamDomain>& args, std::vector<RamDomain>& ret) {
 if (performIO) {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s"},{"fact-dir","."},{"name","unicode"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"s\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"s:symbol\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s"},{"auxArity","0"},{"fact-dir","."},{"name","unicode"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"s\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"s:symbol\"]}}"}});
 if (!inputDirectory.empty()) {directiveMap["fact-dir"] = inputDirectory;}
-IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_4_unicode);
+IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_3_unicode);
 } catch (std::exception& e) {std::cerr << "Error loading data: " << e.what() << '\n';}
 }
-SignalHandler::instance()->setMsg(R"_(unicode("∀").
-in file /home/luc/souffle-haskell/tests/fixtures/edge_cases.dl [30:1-30:16])_");
+signalHandler->setMsg(R"_(unicode("∀").
+in file /home/luc/personal/souffle-haskell/tests/fixtures/edge_cases.dl [30:1-30:16])_");
 [&](){
-CREATE_OP_CONTEXT(rel_4_unicode_op_ctxt,rel_4_unicode->createContext());
+CREATE_OP_CONTEXT(rel_3_unicode_op_ctxt,rel_3_unicode->createContext());
 Tuple<RamDomain,1> tuple{{ramBitCast(RamSigned(3))}};
-rel_4_unicode->insert(tuple,READ_OP_CONTEXT(rel_4_unicode_op_ctxt));
+rel_3_unicode->insert(tuple,READ_OP_CONTEXT(rel_3_unicode_op_ctxt));
 }
-();SignalHandler::instance()->setMsg(R"_(unicode("∀∀").
-in file /home/luc/souffle-haskell/tests/fixtures/edge_cases.dl [31:1-31:19])_");
+();signalHandler->setMsg(R"_(unicode("∀∀").
+in file /home/luc/personal/souffle-haskell/tests/fixtures/edge_cases.dl [31:1-31:19])_");
 [&](){
-CREATE_OP_CONTEXT(rel_4_unicode_op_ctxt,rel_4_unicode->createContext());
+CREATE_OP_CONTEXT(rel_3_unicode_op_ctxt,rel_3_unicode->createContext());
 Tuple<RamDomain,1> tuple{{ramBitCast(RamSigned(4))}};
-rel_4_unicode->insert(tuple,READ_OP_CONTEXT(rel_4_unicode_op_ctxt));
+rel_3_unicode->insert(tuple,READ_OP_CONTEXT(rel_3_unicode_op_ctxt));
 }
 ();if (performIO) {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s"},{"name","unicode"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"s\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"s:symbol\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","s"},{"auxArity","0"},{"name","unicode"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"s\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"s:symbol\"]}}"}});
 if (!outputDirectory.empty()) {directiveMap["output-dir"] = outputDirectory;}
-IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_4_unicode);
+IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_3_unicode);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 }
 }
@@ -628,29 +644,29 @@
 #endif // _MSC_VER
 void subroutine_3(const std::vector<RamDomain>& args, std::vector<RamDomain>& ret) {
 if (performIO) {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","u\tn\tf"},{"fact-dir","."},{"name","no_strings"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 3, \"auxArity\": 0, \"params\": [\"u\", \"n\", \"f\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 3, \"auxArity\": 0, \"types\": [\"u:unsigned\", \"i:number\", \"f:float\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","u\tn\tf"},{"auxArity","0"},{"fact-dir","."},{"name","no_strings"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 3, \"params\": [\"u\", \"n\", \"f\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 3, \"types\": [\"u:unsigned\", \"i:number\", \"f:float\"]}}"}});
 if (!inputDirectory.empty()) {directiveMap["fact-dir"] = inputDirectory;}
-IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_3_no_strings);
+IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_4_no_strings);
 } catch (std::exception& e) {std::cerr << "Error loading data: " << e.what() << '\n';}
 }
-SignalHandler::instance()->setMsg(R"_(no_strings(42,-100,1.5).
-in file /home/luc/souffle-haskell/tests/fixtures/edge_cases.dl [33:1-33:27])_");
+signalHandler->setMsg(R"_(no_strings(42,-100,1.5).
+in file /home/luc/personal/souffle-haskell/tests/fixtures/edge_cases.dl [33:1-33:27])_");
 [&](){
-CREATE_OP_CONTEXT(rel_3_no_strings_op_ctxt,rel_3_no_strings->createContext());
+CREATE_OP_CONTEXT(rel_4_no_strings_op_ctxt,rel_4_no_strings->createContext());
 Tuple<RamDomain,3> tuple{{ramBitCast(RamUnsigned(42)),ramBitCast(RamSigned(-100)),ramBitCast(RamFloat(1.5))}};
-rel_3_no_strings->insert(tuple,READ_OP_CONTEXT(rel_3_no_strings_op_ctxt));
+rel_4_no_strings->insert(tuple,READ_OP_CONTEXT(rel_4_no_strings_op_ctxt));
 }
-();SignalHandler::instance()->setMsg(R"_(no_strings(123,-456,3.14).
-in file /home/luc/souffle-haskell/tests/fixtures/edge_cases.dl [34:1-34:29])_");
+();signalHandler->setMsg(R"_(no_strings(123,-456,3.14).
+in file /home/luc/personal/souffle-haskell/tests/fixtures/edge_cases.dl [34:1-34:29])_");
 [&](){
-CREATE_OP_CONTEXT(rel_3_no_strings_op_ctxt,rel_3_no_strings->createContext());
+CREATE_OP_CONTEXT(rel_4_no_strings_op_ctxt,rel_4_no_strings->createContext());
 Tuple<RamDomain,3> tuple{{ramBitCast(RamUnsigned(123)),ramBitCast(RamSigned(-456)),ramBitCast(RamFloat(3.1400001))}};
-rel_3_no_strings->insert(tuple,READ_OP_CONTEXT(rel_3_no_strings_op_ctxt));
+rel_4_no_strings->insert(tuple,READ_OP_CONTEXT(rel_4_no_strings_op_ctxt));
 }
 ();if (performIO) {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","u\tn\tf"},{"name","no_strings"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 3, \"auxArity\": 0, \"params\": [\"u\", \"n\", \"f\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 3, \"auxArity\": 0, \"types\": [\"u:unsigned\", \"i:number\", \"f:float\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","u\tn\tf"},{"auxArity","0"},{"name","no_strings"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 3, \"params\": [\"u\", \"n\", \"f\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 3, \"types\": [\"u:unsigned\", \"i:number\", \"f:float\"]}}"}});
 if (!outputDirectory.empty()) {directiveMap["output-dir"] = outputDirectory;}
-IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_3_no_strings);
+IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_4_no_strings);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 }
 }
@@ -659,7 +675,7 @@
 #endif // _MSC_VER
 };
 SouffleProgram *newInstance_edge_cases(){return new Sf_edge_cases;}
-SymbolTable *getST_edge_cases(SouffleProgram *p){return &reinterpret_cast<Sf_edge_cases*>(p)->symTable;}
+SymbolTable *getST_edge_cases(SouffleProgram *p){return &reinterpret_cast<Sf_edge_cases*>(p)->getSymbolTable();}
 
 #ifdef __EMBEDDED_SOUFFLE__
 class factory_Sf_edge_cases: public souffle::ProgramFactory {
diff --git a/tests/fixtures/path.cpp b/tests/fixtures/path.cpp
--- a/tests/fixtures/path.cpp
+++ b/tests/fixtures/path.cpp
@@ -6,7 +6,8 @@
 
 namespace souffle {
 static const RamDomain RAM_BIT_SHIFT_MASK = RAM_DOMAIN_SIZE - 1;
-struct t_btree_ii__0_1__11__10 {
+struct t_btree_ii__0_1__11 {
+static constexpr Relation::arity_type Arity = 2;
 using t_tuple = Tuple<RamDomain, 2>;
 struct t_comparator_0{
  int operator()(const t_tuple& a, const t_tuple& b) const {
@@ -88,18 +89,6 @@
 context h;
 return lowerUpperRange_11(lower,upper,h);
 }
-range<t_ind_0::iterator> lowerUpperRange_10(const t_tuple& lower, const t_tuple& upper, context& h) const {
-t_comparator_0 comparator;
-int cmp = comparator(lower, upper);
-if (cmp > 0) {
-    return make_range(ind_0.end(), ind_0.end());
-}
-return make_range(ind_0.lower_bound(lower, h.hints_0_lower), ind_0.upper_bound(upper, h.hints_0_upper));
-}
-range<t_ind_0::iterator> lowerUpperRange_10(const t_tuple& lower, const t_tuple& upper) const {
-context h;
-return lowerUpperRange_10(lower,upper,h);
-}
 bool empty() const {
 return ind_0.empty();
 }
@@ -120,7 +109,8 @@
 ind_0.printStats(o);
 }
 };
-struct t_btree_ii__0_1__11 {
+struct t_btree_ii__0_1__11__10 {
+static constexpr Relation::arity_type Arity = 2;
 using t_tuple = Tuple<RamDomain, 2>;
 struct t_comparator_0{
  int operator()(const t_tuple& a, const t_tuple& b) const {
@@ -202,6 +192,18 @@
 context h;
 return lowerUpperRange_11(lower,upper,h);
 }
+range<t_ind_0::iterator> lowerUpperRange_10(const t_tuple& lower, const t_tuple& upper, context& h) const {
+t_comparator_0 comparator;
+int cmp = comparator(lower, upper);
+if (cmp > 0) {
+    return make_range(ind_0.end(), ind_0.end());
+}
+return make_range(ind_0.lower_bound(lower, h.hints_0_lower), ind_0.upper_bound(upper, h.hints_0_upper));
+}
+range<t_ind_0::iterator> lowerUpperRange_10(const t_tuple& lower, const t_tuple& upper) const {
+context h;
+return lowerUpperRange_10(lower,upper,h);
+}
 bool empty() const {
 return ind_0.empty();
 }
@@ -233,7 +235,7 @@
    return result;
 }
 private:
-static inline std::string substr_wrapper(const std::string& str, size_t idx, size_t len) {
+static inline std::string substr_wrapper(const std::string& str, std::size_t idx, std::size_t len) {
    std::string result; 
    try { result = str.substr(idx,len); } catch(...) { 
      std::cerr << "warning: wrong index position provided by substr(\"";
@@ -248,42 +250,49 @@
 	R"_(c)_",
 };// -- initialize record table --
 RecordTable recordTable;
-// -- Table: @delta_reachable
-Own<t_btree_ii__0_1__11__10> rel_1_delta_reachable = mk<t_btree_ii__0_1__11__10>();
-// -- Table: @new_reachable
-Own<t_btree_ii__0_1__11__10> rel_2_new_reachable = mk<t_btree_ii__0_1__11__10>();
 // -- Table: edge
-Own<t_btree_ii__0_1__11> rel_3_edge = mk<t_btree_ii__0_1__11>();
-souffle::RelationWrapper<0,t_btree_ii__0_1__11,Tuple<RamDomain,2>,2,0> wrapper_rel_3_edge;
+Own<t_btree_ii__0_1__11> rel_1_edge = mk<t_btree_ii__0_1__11>();
+souffle::RelationWrapper<t_btree_ii__0_1__11> wrapper_rel_1_edge;
 // -- Table: reachable
-Own<t_btree_ii__0_1__11> rel_4_reachable = mk<t_btree_ii__0_1__11>();
-souffle::RelationWrapper<1,t_btree_ii__0_1__11,Tuple<RamDomain,2>,2,0> wrapper_rel_4_reachable;
+Own<t_btree_ii__0_1__11> rel_2_reachable = mk<t_btree_ii__0_1__11>();
+souffle::RelationWrapper<t_btree_ii__0_1__11> wrapper_rel_2_reachable;
+// -- Table: @delta_reachable
+Own<t_btree_ii__0_1__11__10> rel_3_delta_reachable = mk<t_btree_ii__0_1__11__10>();
+// -- Table: @new_reachable
+Own<t_btree_ii__0_1__11__10> rel_4_new_reachable = mk<t_btree_ii__0_1__11__10>();
 public:
-Sf_path() : 
-wrapper_rel_3_edge(*rel_3_edge,symTable,"edge",std::array<const char *,2>{{"s:symbol","s:symbol"}},std::array<const char *,2>{{"n","m"}}),
-
-wrapper_rel_4_reachable(*rel_4_reachable,symTable,"reachable",std::array<const char *,2>{{"s:symbol","s:symbol"}},std::array<const char *,2>{{"n","m"}}){
-addRelation("edge",&wrapper_rel_3_edge,true,true);
-addRelation("reachable",&wrapper_rel_4_reachable,false,true);
+Sf_path()
+: wrapper_rel_1_edge(0, *rel_1_edge, *this, "edge", std::array<const char *,2>{{"s:symbol","s:symbol"}}, std::array<const char *,2>{{"n","m"}}, 0)
+, wrapper_rel_2_reachable(1, *rel_2_reachable, *this, "reachable", std::array<const char *,2>{{"s:symbol","s:symbol"}}, std::array<const char *,2>{{"n","m"}}, 0)
+{
+addRelation("edge", wrapper_rel_1_edge, true, true);
+addRelation("reachable", wrapper_rel_2_reachable, false, true);
 }
 ~Sf_path() {
 }
+
 private:
-std::string inputDirectory;
-std::string outputDirectory;
-bool performIO;
-std::atomic<RamDomain> ctr{};
+std::string             inputDirectory;
+std::string             outputDirectory;
+SignalHandler*          signalHandler {SignalHandler::instance()};
+std::atomic<RamDomain>  ctr {};
+std::atomic<std::size_t>     iter {};
+bool                    performIO = false;
 
-std::atomic<size_t> iter{};
-void runFunction(std::string inputDirectoryArg = "", std::string outputDirectoryArg = "", bool performIOArg = false) {
-this->inputDirectory = inputDirectoryArg;
-this->outputDirectory = outputDirectoryArg;
-this->performIO = performIOArg;
-SignalHandler::instance()->set();
+void runFunction(std::string  inputDirectoryArg   = "",
+                 std::string  outputDirectoryArg  = "",
+                 bool         performIOArg        = false) {
+    this->inputDirectory  = std::move(inputDirectoryArg);
+    this->outputDirectory = std::move(outputDirectoryArg);
+    this->performIO       = performIOArg;
+
+    // set default threads (in embedded mode)
+    // if this is not set, and omp is used, the default omp setting of number of cores is used.
 #if defined(_OPENMP)
-if (getNumThreads() > 0) {omp_set_num_threads(getNumThreads());}
+    if (0 < getNumThreads()) { omp_set_num_threads(getNumThreads()); }
 #endif
 
+    signalHandler->set();
 // -- query evaluation --
 {
  std::vector<RamDomain> args, ret;
@@ -295,7 +304,7 @@
 }
 
 // -- relation hint statistics --
-SignalHandler::instance()->reset();
+signalHandler->reset();
 }
 public:
 void run() override { runFunction("", "", false); }
@@ -304,20 +313,20 @@
 }
 public:
 void printAll(std::string outputDirectoryArg = "") override {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","n\tm"},{"name","edge"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 2, \"auxArity\": 0, \"params\": [\"n\", \"m\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 2, \"auxArity\": 0, \"types\": [\"s:symbol\", \"s:symbol\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","n\tm"},{"auxArity","0"},{"name","edge"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 2, \"params\": [\"n\", \"m\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 2, \"types\": [\"s:symbol\", \"s:symbol\"]}}"}});
 if (!outputDirectoryArg.empty()) {directiveMap["output-dir"] = outputDirectoryArg;}
-IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_3_edge);
+IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_1_edge);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","n\tm"},{"name","reachable"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 2, \"auxArity\": 0, \"params\": [\"n\", \"m\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 2, \"auxArity\": 0, \"types\": [\"s:symbol\", \"s:symbol\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","n\tm"},{"auxArity","0"},{"name","reachable"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 2, \"params\": [\"n\", \"m\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 2, \"types\": [\"s:symbol\", \"s:symbol\"]}}"}});
 if (!outputDirectoryArg.empty()) {directiveMap["output-dir"] = outputDirectoryArg;}
-IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_4_reachable);
+IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_2_reachable);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 }
 public:
 void loadAll(std::string inputDirectoryArg = "") override {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","n\tm"},{"fact-dir","."},{"name","edge"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 2, \"auxArity\": 0, \"params\": [\"n\", \"m\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 2, \"auxArity\": 0, \"types\": [\"s:symbol\", \"s:symbol\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","n\tm"},{"auxArity","0"},{"fact-dir","."},{"name","edge"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 2, \"params\": [\"n\", \"m\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 2, \"types\": [\"s:symbol\", \"s:symbol\"]}}"}});
 if (!inputDirectoryArg.empty()) {directiveMap["fact-dir"] = inputDirectoryArg;}
-IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_3_edge);
+IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_1_edge);
 } catch (std::exception& e) {std::cerr << "Error loading data: " << e.what() << '\n';}
 }
 public:
@@ -326,7 +335,7 @@
 rwOperation["IO"] = "stdout";
 rwOperation["name"] = "edge";
 rwOperation["types"] = "{\"relation\": {\"arity\": 2, \"auxArity\": 0, \"types\": [\"s:symbol\", \"s:symbol\"]}}";
-IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_3_edge);
+IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_1_edge);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 }
 public:
@@ -335,19 +344,27 @@
 rwOperation["IO"] = "stdout";
 rwOperation["name"] = "edge";
 rwOperation["types"] = "{\"relation\": {\"arity\": 2, \"auxArity\": 0, \"types\": [\"s:symbol\", \"s:symbol\"]}}";
-IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_3_edge);
+IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_1_edge);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 try {std::map<std::string, std::string> rwOperation;
 rwOperation["IO"] = "stdout";
 rwOperation["name"] = "reachable";
 rwOperation["types"] = "{\"relation\": {\"arity\": 2, \"auxArity\": 0, \"types\": [\"s:symbol\", \"s:symbol\"]}}";
-IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_4_reachable);
+IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_2_reachable);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 }
 public:
 SymbolTable& getSymbolTable() override {
 return symTable;
 }
+RecordTable& getRecordTable() override {
+return recordTable;
+}
+void setNumThreads(std::size_t numThreadsValue) override {
+SouffleProgram::setNumThreads(numThreadsValue);
+symTable.setNumLanes(getNumThreads());
+recordTable.setNumLanes(getNumThreads());
+}
 void executeSubroutine(std::string name, const std::vector<RamDomain>& args, std::vector<RamDomain>& ret) override {
 if (name == "stratum_0") {
 subroutine_0(args, ret);
@@ -362,29 +379,29 @@
 #endif // _MSC_VER
 void subroutine_0(const std::vector<RamDomain>& args, std::vector<RamDomain>& ret) {
 if (performIO) {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","n\tm"},{"fact-dir","."},{"name","edge"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 2, \"auxArity\": 0, \"params\": [\"n\", \"m\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 2, \"auxArity\": 0, \"types\": [\"s:symbol\", \"s:symbol\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","n\tm"},{"auxArity","0"},{"fact-dir","."},{"name","edge"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 2, \"params\": [\"n\", \"m\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 2, \"types\": [\"s:symbol\", \"s:symbol\"]}}"}});
 if (!inputDirectory.empty()) {directiveMap["fact-dir"] = inputDirectory;}
-IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_3_edge);
+IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_1_edge);
 } catch (std::exception& e) {std::cerr << "Error loading data: " << e.what() << '\n';}
 }
-SignalHandler::instance()->setMsg(R"_(edge("a","b").
-in file /Users/luc/personal/souffle-hs/tests/fixtures/path.dl [11:1-11:16])_");
+signalHandler->setMsg(R"_(edge("a","b").
+in file /home/luc/personal/souffle-haskell/tests/fixtures/path.dl [11:1-11:16])_");
 [&](){
-CREATE_OP_CONTEXT(rel_3_edge_op_ctxt,rel_3_edge->createContext());
+CREATE_OP_CONTEXT(rel_1_edge_op_ctxt,rel_1_edge->createContext());
 Tuple<RamDomain,2> tuple{{ramBitCast(RamSigned(0)),ramBitCast(RamSigned(1))}};
-rel_3_edge->insert(tuple,READ_OP_CONTEXT(rel_3_edge_op_ctxt));
+rel_1_edge->insert(tuple,READ_OP_CONTEXT(rel_1_edge_op_ctxt));
 }
-();SignalHandler::instance()->setMsg(R"_(edge("b","c").
-in file /Users/luc/personal/souffle-hs/tests/fixtures/path.dl [12:1-12:16])_");
+();signalHandler->setMsg(R"_(edge("b","c").
+in file /home/luc/personal/souffle-haskell/tests/fixtures/path.dl [12:1-12:16])_");
 [&](){
-CREATE_OP_CONTEXT(rel_3_edge_op_ctxt,rel_3_edge->createContext());
+CREATE_OP_CONTEXT(rel_1_edge_op_ctxt,rel_1_edge->createContext());
 Tuple<RamDomain,2> tuple{{ramBitCast(RamSigned(1)),ramBitCast(RamSigned(2))}};
-rel_3_edge->insert(tuple,READ_OP_CONTEXT(rel_3_edge_op_ctxt));
+rel_1_edge->insert(tuple,READ_OP_CONTEXT(rel_1_edge_op_ctxt));
 }
 ();if (performIO) {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","n\tm"},{"name","edge"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 2, \"auxArity\": 0, \"params\": [\"n\", \"m\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 2, \"auxArity\": 0, \"types\": [\"s:symbol\", \"s:symbol\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","n\tm"},{"auxArity","0"},{"name","edge"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 2, \"params\": [\"n\", \"m\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 2, \"types\": [\"s:symbol\", \"s:symbol\"]}}"}});
 if (!outputDirectory.empty()) {directiveMap["output-dir"] = outputDirectory;}
-IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_3_edge);
+IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_1_edge);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 }
 }
@@ -395,81 +412,81 @@
 #pragma warning(disable: 4100)
 #endif // _MSC_VER
 void subroutine_1(const std::vector<RamDomain>& args, std::vector<RamDomain>& ret) {
-SignalHandler::instance()->setMsg(R"_(reachable(x,y) :- 
+signalHandler->setMsg(R"_(reachable(x,y) :- 
    edge(x,y).
-in file /Users/luc/personal/souffle-hs/tests/fixtures/path.dl [14:1-14:31])_");
-if(!(rel_3_edge->empty())) {
+in file /home/luc/personal/souffle-haskell/tests/fixtures/path.dl [14:1-14:31])_");
+if(!(rel_1_edge->empty())) {
 [&](){
-CREATE_OP_CONTEXT(rel_3_edge_op_ctxt,rel_3_edge->createContext());
-CREATE_OP_CONTEXT(rel_4_reachable_op_ctxt,rel_4_reachable->createContext());
-for(const auto& env0 : *rel_3_edge) {
+CREATE_OP_CONTEXT(rel_2_reachable_op_ctxt,rel_2_reachable->createContext());
+CREATE_OP_CONTEXT(rel_1_edge_op_ctxt,rel_1_edge->createContext());
+for(const auto& env0 : *rel_1_edge) {
 Tuple<RamDomain,2> tuple{{ramBitCast(env0[0]),ramBitCast(env0[1])}};
-rel_4_reachable->insert(tuple,READ_OP_CONTEXT(rel_4_reachable_op_ctxt));
+rel_2_reachable->insert(tuple,READ_OP_CONTEXT(rel_2_reachable_op_ctxt));
 }
 }
 ();}
 [&](){
-CREATE_OP_CONTEXT(rel_4_reachable_op_ctxt,rel_4_reachable->createContext());
-CREATE_OP_CONTEXT(rel_1_delta_reachable_op_ctxt,rel_1_delta_reachable->createContext());
-for(const auto& env0 : *rel_4_reachable) {
+CREATE_OP_CONTEXT(rel_3_delta_reachable_op_ctxt,rel_3_delta_reachable->createContext());
+CREATE_OP_CONTEXT(rel_2_reachable_op_ctxt,rel_2_reachable->createContext());
+for(const auto& env0 : *rel_2_reachable) {
 Tuple<RamDomain,2> tuple{{ramBitCast(env0[0]),ramBitCast(env0[1])}};
-rel_1_delta_reachable->insert(tuple,READ_OP_CONTEXT(rel_1_delta_reachable_op_ctxt));
+rel_3_delta_reachable->insert(tuple,READ_OP_CONTEXT(rel_3_delta_reachable_op_ctxt));
 }
 }
 ();iter = 0;
 for(;;) {
-SignalHandler::instance()->setMsg(R"_(reachable(x,z) :- 
+signalHandler->setMsg(R"_(reachable(x,z) :- 
    edge(x,y),
    reachable(y,z).
-in file /Users/luc/personal/souffle-hs/tests/fixtures/path.dl [15:1-15:48])_");
-if(!(rel_3_edge->empty()) && !(rel_1_delta_reachable->empty())) {
+in file /home/luc/personal/souffle-haskell/tests/fixtures/path.dl [15:1-15:48])_");
+if(!(rel_1_edge->empty()) && !(rel_3_delta_reachable->empty())) {
 [&](){
-CREATE_OP_CONTEXT(rel_3_edge_op_ctxt,rel_3_edge->createContext());
-CREATE_OP_CONTEXT(rel_4_reachable_op_ctxt,rel_4_reachable->createContext());
-CREATE_OP_CONTEXT(rel_1_delta_reachable_op_ctxt,rel_1_delta_reachable->createContext());
-CREATE_OP_CONTEXT(rel_2_new_reachable_op_ctxt,rel_2_new_reachable->createContext());
-for(const auto& env0 : *rel_3_edge) {
-auto range = rel_1_delta_reachable->lowerUpperRange_10(Tuple<RamDomain,2>{{ramBitCast(env0[1]), ramBitCast<RamDomain>(MIN_RAM_SIGNED)}},Tuple<RamDomain,2>{{ramBitCast(env0[1]), ramBitCast<RamDomain>(MAX_RAM_SIGNED)}},READ_OP_CONTEXT(rel_1_delta_reachable_op_ctxt));
+CREATE_OP_CONTEXT(rel_3_delta_reachable_op_ctxt,rel_3_delta_reachable->createContext());
+CREATE_OP_CONTEXT(rel_2_reachable_op_ctxt,rel_2_reachable->createContext());
+CREATE_OP_CONTEXT(rel_1_edge_op_ctxt,rel_1_edge->createContext());
+CREATE_OP_CONTEXT(rel_4_new_reachable_op_ctxt,rel_4_new_reachable->createContext());
+for(const auto& env0 : *rel_1_edge) {
+auto range = rel_3_delta_reachable->lowerUpperRange_10(Tuple<RamDomain,2>{{ramBitCast(env0[1]), ramBitCast<RamDomain>(MIN_RAM_SIGNED)}},Tuple<RamDomain,2>{{ramBitCast(env0[1]), ramBitCast<RamDomain>(MAX_RAM_SIGNED)}},READ_OP_CONTEXT(rel_3_delta_reachable_op_ctxt));
 for(const auto& env1 : range) {
-if( !(rel_4_reachable->contains(Tuple<RamDomain,2>{{ramBitCast(env0[0]),ramBitCast(env1[1])}},READ_OP_CONTEXT(rel_4_reachable_op_ctxt)))) {
+if( !(rel_2_reachable->contains(Tuple<RamDomain,2>{{ramBitCast(env0[0]),ramBitCast(env1[1])}},READ_OP_CONTEXT(rel_2_reachable_op_ctxt)))) {
 Tuple<RamDomain,2> tuple{{ramBitCast(env0[0]),ramBitCast(env1[1])}};
-rel_2_new_reachable->insert(tuple,READ_OP_CONTEXT(rel_2_new_reachable_op_ctxt));
+rel_4_new_reachable->insert(tuple,READ_OP_CONTEXT(rel_4_new_reachable_op_ctxt));
 }
 }
 }
 }
 ();}
-if(rel_2_new_reachable->empty()) break;
+if(rel_4_new_reachable->empty()) break;
 [&](){
-CREATE_OP_CONTEXT(rel_4_reachable_op_ctxt,rel_4_reachable->createContext());
-CREATE_OP_CONTEXT(rel_2_new_reachable_op_ctxt,rel_2_new_reachable->createContext());
-for(const auto& env0 : *rel_2_new_reachable) {
+CREATE_OP_CONTEXT(rel_2_reachable_op_ctxt,rel_2_reachable->createContext());
+CREATE_OP_CONTEXT(rel_4_new_reachable_op_ctxt,rel_4_new_reachable->createContext());
+for(const auto& env0 : *rel_4_new_reachable) {
 Tuple<RamDomain,2> tuple{{ramBitCast(env0[0]),ramBitCast(env0[1])}};
-rel_4_reachable->insert(tuple,READ_OP_CONTEXT(rel_4_reachable_op_ctxt));
+rel_2_reachable->insert(tuple,READ_OP_CONTEXT(rel_2_reachable_op_ctxt));
 }
 }
-();std::swap(rel_1_delta_reachable, rel_2_new_reachable);
-rel_2_new_reachable->purge();
+();std::swap(rel_3_delta_reachable, rel_4_new_reachable);
+rel_4_new_reachable->purge();
 iter++;
 }
 iter = 0;
-rel_1_delta_reachable->purge();
-rel_2_new_reachable->purge();
+rel_3_delta_reachable->purge();
+rel_4_new_reachable->purge();
 if (performIO) {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","n\tm"},{"name","reachable"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 2, \"auxArity\": 0, \"params\": [\"n\", \"m\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 2, \"auxArity\": 0, \"types\": [\"s:symbol\", \"s:symbol\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","n\tm"},{"auxArity","0"},{"name","reachable"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 2, \"params\": [\"n\", \"m\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 2, \"types\": [\"s:symbol\", \"s:symbol\"]}}"}});
 if (!outputDirectory.empty()) {directiveMap["output-dir"] = outputDirectory;}
-IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_4_reachable);
+IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_2_reachable);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 }
-if (performIO) rel_4_reachable->purge();
-if (performIO) rel_3_edge->purge();
+if (performIO) rel_2_reachable->purge();
+if (performIO) rel_1_edge->purge();
 }
 #ifdef _MSC_VER
 #pragma warning(default: 4100)
 #endif // _MSC_VER
 };
 SouffleProgram *newInstance_path(){return new Sf_path;}
-SymbolTable *getST_path(SouffleProgram *p){return &reinterpret_cast<Sf_path*>(p)->symTable;}
+SymbolTable *getST_path(SouffleProgram *p){return &reinterpret_cast<Sf_path*>(p)->getSymbolTable();}
 
 #ifdef __EMBEDDED_SOUFFLE__
 class factory_Sf_path: public souffle::ProgramFactory {
diff --git a/tests/fixtures/round_trip.cpp b/tests/fixtures/round_trip.cpp
--- a/tests/fixtures/round_trip.cpp
+++ b/tests/fixtures/round_trip.cpp
@@ -6,17 +6,18 @@
 
 namespace souffle {
 static const RamDomain RAM_BIT_SHIFT_MASK = RAM_DOMAIN_SIZE - 1;
-struct t_btree_f__0__1 {
+struct t_btree_i__0__1 {
+static constexpr Relation::arity_type Arity = 1;
 using t_tuple = Tuple<RamDomain, 1>;
 struct t_comparator_0{
  int operator()(const t_tuple& a, const t_tuple& b) const {
-  return (ramBitCast<RamFloat>(a[0]) < ramBitCast<RamFloat>(b[0])) ? -1 : (ramBitCast<RamFloat>(a[0]) > ramBitCast<RamFloat>(b[0])) ? 1 :(0);
+  return (ramBitCast<RamSigned>(a[0]) < ramBitCast<RamSigned>(b[0])) ? -1 : (ramBitCast<RamSigned>(a[0]) > ramBitCast<RamSigned>(b[0])) ? 1 :(0);
  }
 bool less(const t_tuple& a, const t_tuple& b) const {
-  return (ramBitCast<RamFloat>(a[0]) < ramBitCast<RamFloat>(b[0]));
+  return (ramBitCast<RamSigned>(a[0]) < ramBitCast<RamSigned>(b[0]));
  }
 bool equal(const t_tuple& a, const t_tuple& b) const {
-return (ramBitCast<RamFloat>(a[0]) == ramBitCast<RamFloat>(b[0]));
+return (ramBitCast<RamSigned>(a[0]) == ramBitCast<RamSigned>(b[0]));
  }
 };
 using t_ind_0 = btree_set<t_tuple,t_comparator_0>;
@@ -108,17 +109,18 @@
 ind_0.printStats(o);
 }
 };
-struct t_btree_i__0__1 {
+struct t_btree_u__0__1 {
+static constexpr Relation::arity_type Arity = 1;
 using t_tuple = Tuple<RamDomain, 1>;
 struct t_comparator_0{
  int operator()(const t_tuple& a, const t_tuple& b) const {
-  return (ramBitCast<RamSigned>(a[0]) < ramBitCast<RamSigned>(b[0])) ? -1 : (ramBitCast<RamSigned>(a[0]) > ramBitCast<RamSigned>(b[0])) ? 1 :(0);
+  return (ramBitCast<RamUnsigned>(a[0]) < ramBitCast<RamUnsigned>(b[0])) ? -1 : (ramBitCast<RamUnsigned>(a[0]) > ramBitCast<RamUnsigned>(b[0])) ? 1 :(0);
  }
 bool less(const t_tuple& a, const t_tuple& b) const {
-  return (ramBitCast<RamSigned>(a[0]) < ramBitCast<RamSigned>(b[0]));
+  return (ramBitCast<RamUnsigned>(a[0]) < ramBitCast<RamUnsigned>(b[0]));
  }
 bool equal(const t_tuple& a, const t_tuple& b) const {
-return (ramBitCast<RamSigned>(a[0]) == ramBitCast<RamSigned>(b[0]));
+return (ramBitCast<RamUnsigned>(a[0]) == ramBitCast<RamUnsigned>(b[0]));
  }
 };
 using t_ind_0 = btree_set<t_tuple,t_comparator_0>;
@@ -210,17 +212,18 @@
 ind_0.printStats(o);
 }
 };
-struct t_btree_u__0__1 {
+struct t_btree_f__0__1 {
+static constexpr Relation::arity_type Arity = 1;
 using t_tuple = Tuple<RamDomain, 1>;
 struct t_comparator_0{
  int operator()(const t_tuple& a, const t_tuple& b) const {
-  return (ramBitCast<RamUnsigned>(a[0]) < ramBitCast<RamUnsigned>(b[0])) ? -1 : (ramBitCast<RamUnsigned>(a[0]) > ramBitCast<RamUnsigned>(b[0])) ? 1 :(0);
+  return (ramBitCast<RamFloat>(a[0]) < ramBitCast<RamFloat>(b[0])) ? -1 : (ramBitCast<RamFloat>(a[0]) > ramBitCast<RamFloat>(b[0])) ? 1 :(0);
  }
 bool less(const t_tuple& a, const t_tuple& b) const {
-  return (ramBitCast<RamUnsigned>(a[0]) < ramBitCast<RamUnsigned>(b[0]));
+  return (ramBitCast<RamFloat>(a[0]) < ramBitCast<RamFloat>(b[0]));
  }
 bool equal(const t_tuple& a, const t_tuple& b) const {
-return (ramBitCast<RamUnsigned>(a[0]) == ramBitCast<RamUnsigned>(b[0]));
+return (ramBitCast<RamFloat>(a[0]) == ramBitCast<RamFloat>(b[0]));
  }
 };
 using t_ind_0 = btree_set<t_tuple,t_comparator_0>;
@@ -323,7 +326,7 @@
    return result;
 }
 private:
-static inline std::string substr_wrapper(const std::string& str, size_t idx, size_t len) {
+static inline std::string substr_wrapper(const std::string& str, std::size_t idx, std::size_t len) {
    std::string result; 
    try { result = str.substr(idx,len); } catch(...) { 
      std::cerr << "warning: wrong index position provided by substr(\"";
@@ -334,50 +337,55 @@
 // -- initialize symbol table --
 SymbolTable symTable;// -- initialize record table --
 RecordTable recordTable;
-// -- Table: float_fact
-Own<t_btree_f__0__1> rel_1_float_fact = mk<t_btree_f__0__1>();
-souffle::RelationWrapper<0,t_btree_f__0__1,Tuple<RamDomain,1>,1,0> wrapper_rel_1_float_fact;
+// -- Table: string_fact
+Own<t_btree_i__0__1> rel_1_string_fact = mk<t_btree_i__0__1>();
+souffle::RelationWrapper<t_btree_i__0__1> wrapper_rel_1_string_fact;
 // -- Table: number_fact
 Own<t_btree_i__0__1> rel_2_number_fact = mk<t_btree_i__0__1>();
-souffle::RelationWrapper<1,t_btree_i__0__1,Tuple<RamDomain,1>,1,0> wrapper_rel_2_number_fact;
-// -- Table: string_fact
-Own<t_btree_i__0__1> rel_3_string_fact = mk<t_btree_i__0__1>();
-souffle::RelationWrapper<2,t_btree_i__0__1,Tuple<RamDomain,1>,1,0> wrapper_rel_3_string_fact;
+souffle::RelationWrapper<t_btree_i__0__1> wrapper_rel_2_number_fact;
 // -- Table: unsigned_fact
-Own<t_btree_u__0__1> rel_4_unsigned_fact = mk<t_btree_u__0__1>();
-souffle::RelationWrapper<3,t_btree_u__0__1,Tuple<RamDomain,1>,1,0> wrapper_rel_4_unsigned_fact;
+Own<t_btree_u__0__1> rel_3_unsigned_fact = mk<t_btree_u__0__1>();
+souffle::RelationWrapper<t_btree_u__0__1> wrapper_rel_3_unsigned_fact;
+// -- Table: float_fact
+Own<t_btree_f__0__1> rel_4_float_fact = mk<t_btree_f__0__1>();
+souffle::RelationWrapper<t_btree_f__0__1> wrapper_rel_4_float_fact;
 public:
-Sf_round_trip() : 
-wrapper_rel_1_float_fact(*rel_1_float_fact,symTable,"float_fact",std::array<const char *,1>{{"f:float"}},std::array<const char *,1>{{"x"}}),
-
-wrapper_rel_2_number_fact(*rel_2_number_fact,symTable,"number_fact",std::array<const char *,1>{{"i:number"}},std::array<const char *,1>{{"x"}}),
-
-wrapper_rel_3_string_fact(*rel_3_string_fact,symTable,"string_fact",std::array<const char *,1>{{"s:symbol"}},std::array<const char *,1>{{"x"}}),
-
-wrapper_rel_4_unsigned_fact(*rel_4_unsigned_fact,symTable,"unsigned_fact",std::array<const char *,1>{{"u:unsigned"}},std::array<const char *,1>{{"x"}}){
-addRelation("float_fact",&wrapper_rel_1_float_fact,true,true);
-addRelation("number_fact",&wrapper_rel_2_number_fact,true,true);
-addRelation("string_fact",&wrapper_rel_3_string_fact,true,true);
-addRelation("unsigned_fact",&wrapper_rel_4_unsigned_fact,true,true);
+Sf_round_trip()
+: wrapper_rel_1_string_fact(0, *rel_1_string_fact, *this, "string_fact", std::array<const char *,1>{{"s:symbol"}}, std::array<const char *,1>{{"x"}}, 0)
+, wrapper_rel_2_number_fact(1, *rel_2_number_fact, *this, "number_fact", std::array<const char *,1>{{"i:number"}}, std::array<const char *,1>{{"x"}}, 0)
+, wrapper_rel_3_unsigned_fact(2, *rel_3_unsigned_fact, *this, "unsigned_fact", std::array<const char *,1>{{"u:unsigned"}}, std::array<const char *,1>{{"x"}}, 0)
+, wrapper_rel_4_float_fact(3, *rel_4_float_fact, *this, "float_fact", std::array<const char *,1>{{"f:float"}}, std::array<const char *,1>{{"x"}}, 0)
+{
+addRelation("string_fact", wrapper_rel_1_string_fact, true, true);
+addRelation("number_fact", wrapper_rel_2_number_fact, true, true);
+addRelation("unsigned_fact", wrapper_rel_3_unsigned_fact, true, true);
+addRelation("float_fact", wrapper_rel_4_float_fact, true, true);
 }
 ~Sf_round_trip() {
 }
+
 private:
-std::string inputDirectory;
-std::string outputDirectory;
-bool performIO;
-std::atomic<RamDomain> ctr{};
+std::string             inputDirectory;
+std::string             outputDirectory;
+SignalHandler*          signalHandler {SignalHandler::instance()};
+std::atomic<RamDomain>  ctr {};
+std::atomic<std::size_t>     iter {};
+bool                    performIO = false;
 
-std::atomic<size_t> iter{};
-void runFunction(std::string inputDirectoryArg = "", std::string outputDirectoryArg = "", bool performIOArg = false) {
-this->inputDirectory = inputDirectoryArg;
-this->outputDirectory = outputDirectoryArg;
-this->performIO = performIOArg;
-SignalHandler::instance()->set();
+void runFunction(std::string  inputDirectoryArg   = "",
+                 std::string  outputDirectoryArg  = "",
+                 bool         performIOArg        = false) {
+    this->inputDirectory  = std::move(inputDirectoryArg);
+    this->outputDirectory = std::move(outputDirectoryArg);
+    this->performIO       = performIOArg;
+
+    // set default threads (in embedded mode)
+    // if this is not set, and omp is used, the default omp setting of number of cores is used.
 #if defined(_OPENMP)
-if (getNumThreads() > 0) {omp_set_num_threads(getNumThreads());}
+    if (0 < getNumThreads()) { omp_set_num_threads(getNumThreads()); }
 #endif
 
+    signalHandler->set();
 // -- query evaluation --
 {
  std::vector<RamDomain> args, ret;
@@ -397,7 +405,7 @@
 }
 
 // -- relation hint statistics --
-SignalHandler::instance()->reset();
+signalHandler->reset();
 }
 public:
 void run() override { runFunction("", "", false); }
@@ -406,49 +414,49 @@
 }
 public:
 void printAll(std::string outputDirectoryArg = "") override {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"name","number_fact"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"i:number\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"auxArity","0"},{"name","string_fact"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"s:symbol\"]}}"}});
 if (!outputDirectoryArg.empty()) {directiveMap["output-dir"] = outputDirectoryArg;}
-IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_2_number_fact);
+IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_1_string_fact);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"name","unsigned_fact"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"u:unsigned\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"auxArity","0"},{"name","number_fact"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"i:number\"]}}"}});
 if (!outputDirectoryArg.empty()) {directiveMap["output-dir"] = outputDirectoryArg;}
-IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_4_unsigned_fact);
+IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_2_number_fact);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"name","string_fact"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"s:symbol\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"auxArity","0"},{"name","unsigned_fact"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"u:unsigned\"]}}"}});
 if (!outputDirectoryArg.empty()) {directiveMap["output-dir"] = outputDirectoryArg;}
-IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_3_string_fact);
+IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_3_unsigned_fact);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"name","float_fact"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"f:float\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"auxArity","0"},{"name","float_fact"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"f:float\"]}}"}});
 if (!outputDirectoryArg.empty()) {directiveMap["output-dir"] = outputDirectoryArg;}
-IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_1_float_fact);
+IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_4_float_fact);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 }
 public:
 void loadAll(std::string inputDirectoryArg = "") override {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"fact-dir","."},{"name","float_fact"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"f:float\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"auxArity","0"},{"fact-dir","."},{"name","string_fact"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"s:symbol\"]}}"}});
 if (!inputDirectoryArg.empty()) {directiveMap["fact-dir"] = inputDirectoryArg;}
-IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_1_float_fact);
+IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_1_string_fact);
 } catch (std::exception& e) {std::cerr << "Error loading data: " << e.what() << '\n';}
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"fact-dir","."},{"name","number_fact"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"i:number\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"auxArity","0"},{"fact-dir","."},{"name","number_fact"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"i:number\"]}}"}});
 if (!inputDirectoryArg.empty()) {directiveMap["fact-dir"] = inputDirectoryArg;}
 IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_2_number_fact);
 } catch (std::exception& e) {std::cerr << "Error loading data: " << e.what() << '\n';}
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"fact-dir","."},{"name","string_fact"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"s:symbol\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"auxArity","0"},{"fact-dir","."},{"name","unsigned_fact"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"u:unsigned\"]}}"}});
 if (!inputDirectoryArg.empty()) {directiveMap["fact-dir"] = inputDirectoryArg;}
-IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_3_string_fact);
+IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_3_unsigned_fact);
 } catch (std::exception& e) {std::cerr << "Error loading data: " << e.what() << '\n';}
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"fact-dir","."},{"name","unsigned_fact"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"u:unsigned\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"auxArity","0"},{"fact-dir","."},{"name","float_fact"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"f:float\"]}}"}});
 if (!inputDirectoryArg.empty()) {directiveMap["fact-dir"] = inputDirectoryArg;}
-IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_4_unsigned_fact);
+IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_4_float_fact);
 } catch (std::exception& e) {std::cerr << "Error loading data: " << e.what() << '\n';}
 }
 public:
 void dumpInputs() override {
 try {std::map<std::string, std::string> rwOperation;
 rwOperation["IO"] = "stdout";
-rwOperation["name"] = "float_fact";
-rwOperation["types"] = "{\"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"f:float\"]}}";
-IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_1_float_fact);
+rwOperation["name"] = "string_fact";
+rwOperation["types"] = "{\"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"s:symbol\"]}}";
+IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_1_string_fact);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 try {std::map<std::string, std::string> rwOperation;
 rwOperation["IO"] = "stdout";
@@ -458,21 +466,27 @@
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 try {std::map<std::string, std::string> rwOperation;
 rwOperation["IO"] = "stdout";
-rwOperation["name"] = "string_fact";
-rwOperation["types"] = "{\"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"s:symbol\"]}}";
-IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_3_string_fact);
+rwOperation["name"] = "unsigned_fact";
+rwOperation["types"] = "{\"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"u:unsigned\"]}}";
+IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_3_unsigned_fact);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 try {std::map<std::string, std::string> rwOperation;
 rwOperation["IO"] = "stdout";
-rwOperation["name"] = "unsigned_fact";
-rwOperation["types"] = "{\"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"u:unsigned\"]}}";
-IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_4_unsigned_fact);
+rwOperation["name"] = "float_fact";
+rwOperation["types"] = "{\"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"f:float\"]}}";
+IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_4_float_fact);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 }
 public:
 void dumpOutputs() override {
 try {std::map<std::string, std::string> rwOperation;
 rwOperation["IO"] = "stdout";
+rwOperation["name"] = "string_fact";
+rwOperation["types"] = "{\"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"s:symbol\"]}}";
+IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_1_string_fact);
+} catch (std::exception& e) {std::cerr << e.what();exit(1);}
+try {std::map<std::string, std::string> rwOperation;
+rwOperation["IO"] = "stdout";
 rwOperation["name"] = "number_fact";
 rwOperation["types"] = "{\"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"i:number\"]}}";
 IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_2_number_fact);
@@ -481,25 +495,27 @@
 rwOperation["IO"] = "stdout";
 rwOperation["name"] = "unsigned_fact";
 rwOperation["types"] = "{\"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"u:unsigned\"]}}";
-IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_4_unsigned_fact);
-} catch (std::exception& e) {std::cerr << e.what();exit(1);}
-try {std::map<std::string, std::string> rwOperation;
-rwOperation["IO"] = "stdout";
-rwOperation["name"] = "string_fact";
-rwOperation["types"] = "{\"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"s:symbol\"]}}";
-IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_3_string_fact);
+IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_3_unsigned_fact);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 try {std::map<std::string, std::string> rwOperation;
 rwOperation["IO"] = "stdout";
 rwOperation["name"] = "float_fact";
 rwOperation["types"] = "{\"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"f:float\"]}}";
-IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_1_float_fact);
+IOSystem::getInstance().getWriter(rwOperation, symTable, recordTable)->writeAll(*rel_4_float_fact);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 }
 public:
 SymbolTable& getSymbolTable() override {
 return symTable;
 }
+RecordTable& getRecordTable() override {
+return recordTable;
+}
+void setNumThreads(std::size_t numThreadsValue) override {
+SouffleProgram::setNumThreads(numThreadsValue);
+symTable.setNumLanes(getNumThreads());
+recordTable.setNumLanes(getNumThreads());
+}
 void executeSubroutine(std::string name, const std::vector<RamDomain>& args, std::vector<RamDomain>& ret) override {
 if (name == "stratum_0") {
 subroutine_0(args, ret);
@@ -520,15 +536,15 @@
 #endif // _MSC_VER
 void subroutine_0(const std::vector<RamDomain>& args, std::vector<RamDomain>& ret) {
 if (performIO) {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"fact-dir","."},{"name","string_fact"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"s:symbol\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"auxArity","0"},{"fact-dir","."},{"name","string_fact"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"s:symbol\"]}}"}});
 if (!inputDirectory.empty()) {directiveMap["fact-dir"] = inputDirectory;}
-IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_3_string_fact);
+IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_1_string_fact);
 } catch (std::exception& e) {std::cerr << "Error loading data: " << e.what() << '\n';}
 }
 if (performIO) {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"name","string_fact"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"s:symbol\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"auxArity","0"},{"name","string_fact"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"s:symbol\"]}}"}});
 if (!outputDirectory.empty()) {directiveMap["output-dir"] = outputDirectory;}
-IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_3_string_fact);
+IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_1_string_fact);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 }
 }
@@ -540,13 +556,13 @@
 #endif // _MSC_VER
 void subroutine_1(const std::vector<RamDomain>& args, std::vector<RamDomain>& ret) {
 if (performIO) {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"fact-dir","."},{"name","number_fact"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"i:number\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"auxArity","0"},{"fact-dir","."},{"name","number_fact"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"i:number\"]}}"}});
 if (!inputDirectory.empty()) {directiveMap["fact-dir"] = inputDirectory;}
 IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_2_number_fact);
 } catch (std::exception& e) {std::cerr << "Error loading data: " << e.what() << '\n';}
 }
 if (performIO) {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"name","number_fact"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"i:number\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"auxArity","0"},{"name","number_fact"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"i:number\"]}}"}});
 if (!outputDirectory.empty()) {directiveMap["output-dir"] = outputDirectory;}
 IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_2_number_fact);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
@@ -560,15 +576,15 @@
 #endif // _MSC_VER
 void subroutine_2(const std::vector<RamDomain>& args, std::vector<RamDomain>& ret) {
 if (performIO) {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"fact-dir","."},{"name","unsigned_fact"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"u:unsigned\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"auxArity","0"},{"fact-dir","."},{"name","unsigned_fact"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"u:unsigned\"]}}"}});
 if (!inputDirectory.empty()) {directiveMap["fact-dir"] = inputDirectory;}
-IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_4_unsigned_fact);
+IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_3_unsigned_fact);
 } catch (std::exception& e) {std::cerr << "Error loading data: " << e.what() << '\n';}
 }
 if (performIO) {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"name","unsigned_fact"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"u:unsigned\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"auxArity","0"},{"name","unsigned_fact"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"u:unsigned\"]}}"}});
 if (!outputDirectory.empty()) {directiveMap["output-dir"] = outputDirectory;}
-IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_4_unsigned_fact);
+IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_3_unsigned_fact);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 }
 }
@@ -580,15 +596,15 @@
 #endif // _MSC_VER
 void subroutine_3(const std::vector<RamDomain>& args, std::vector<RamDomain>& ret) {
 if (performIO) {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"fact-dir","."},{"name","float_fact"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"f:float\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"auxArity","0"},{"fact-dir","."},{"name","float_fact"},{"operation","input"},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"f:float\"]}}"}});
 if (!inputDirectory.empty()) {directiveMap["fact-dir"] = inputDirectory;}
-IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_1_float_fact);
+IOSystem::getInstance().getReader(directiveMap, symTable, recordTable)->readAll(*rel_4_float_fact);
 } catch (std::exception& e) {std::cerr << "Error loading data: " << e.what() << '\n';}
 }
 if (performIO) {
-try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"name","float_fact"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"auxArity\": 0, \"types\": [\"f:float\"]}}"}});
+try {std::map<std::string, std::string> directiveMap({{"IO","file"},{"attributeNames","x"},{"auxArity","0"},{"name","float_fact"},{"operation","output"},{"output-dir","."},{"params","{\"records\": {}, \"relation\": {\"arity\": 1, \"params\": [\"x\"]}}"},{"types","{\"ADTs\": {}, \"records\": {}, \"relation\": {\"arity\": 1, \"types\": [\"f:float\"]}}"}});
 if (!outputDirectory.empty()) {directiveMap["output-dir"] = outputDirectory;}
-IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_1_float_fact);
+IOSystem::getInstance().getWriter(directiveMap, symTable, recordTable)->writeAll(*rel_4_float_fact);
 } catch (std::exception& e) {std::cerr << e.what();exit(1);}
 }
 }
@@ -597,7 +613,7 @@
 #endif // _MSC_VER
 };
 SouffleProgram *newInstance_round_trip(){return new Sf_round_trip;}
-SymbolTable *getST_round_trip(SouffleProgram *p){return &reinterpret_cast<Sf_round_trip*>(p)->symTable;}
+SymbolTable *getST_round_trip(SouffleProgram *p){return &reinterpret_cast<Sf_round_trip*>(p)->getSymbolTable();}
 
 #ifdef __EMBEDDED_SOUFFLE__
 class factory_Sf_round_trip: public souffle::ProgramFactory {
