#ifndef REALM_IMPL_INSTRUCTIONS_HPP #define REALM_IMPL_INSTRUCTIONS_HPP #include #include #include // string conversion, debug prints #include // shared_ptr #include #include #include #include #include #include #include #include #include #include #include namespace realm { namespace sync { #define REALM_FOR_EACH_INSTRUCTION_TYPE(X) \ X(AddTable) \ X(EraseTable) \ X(AddColumn) \ X(EraseColumn) \ X(CreateObject) \ X(EraseObject) \ X(Update) \ X(AddInteger) \ X(ArrayInsert) \ X(ArrayMove) \ X(ArrayErase) \ X(Clear) \ X(SetInsert) \ X(SetErase) struct StringBufferRange { uint32_t offset, size; friend bool operator==(const StringBufferRange& lhs, const StringBufferRange& rhs) noexcept { return lhs.offset == rhs.offset && lhs.size == rhs.size; } }; struct InternString { static const InternString npos; explicit constexpr InternString(uint32_t v = uint32_t(-1)) noexcept : value(v) { } uint32_t value; constexpr bool operator==(const InternString& other) const noexcept { return value == other.value; } constexpr bool operator!=(const InternString& other) const noexcept { return value != other.value; } constexpr bool operator<(const InternString& other) const noexcept { return value < other.value; } explicit operator bool() const noexcept { return (value != npos.value); } }; struct Instruction; namespace instr { using PrimaryKey = mpark::variant; struct Path { using Element = mpark::variant; // FIXME: Use a "small_vector" type for this -- most paths are very short. // Alternatively, we could use some kind of interning with copy-on-write, // but that seems complicated. std::vector m_path; size_t size() const noexcept { return m_path.size(); } // If this path is referring to an element of an array (the last path // element is an integer index), return true. bool is_array_index() const noexcept { return !m_path.empty() && mpark::holds_alternative(m_path.back()); } uint32_t& index() noexcept { REALM_ASSERT(is_array_index()); return mpark::get(m_path.back()); } uint32_t index() const noexcept { REALM_ASSERT(is_array_index()); return mpark::get(m_path.back()); } Element& back() noexcept { REALM_ASSERT(!m_path.empty()); return m_path.back(); } const Element& back() const noexcept { REALM_ASSERT(!m_path.empty()); return m_path.back(); } Element& operator[](size_t idx) noexcept { REALM_ASSERT(idx < m_path.size()); return m_path[idx]; } const Element& operator[](size_t idx) const noexcept { REALM_ASSERT(idx < m_path.size()); return m_path[idx]; } void push_back(Element element) { m_path.push_back(element); } friend bool operator==(const Path& lhs, const Path& rhs) noexcept { return lhs.m_path == rhs.m_path; } using const_iterator = typename std::vector::const_iterator; const_iterator begin() const noexcept { return m_path.begin(); } const_iterator end() const noexcept { return m_path.end(); } }; struct Payload { /// Create a new object in-place (embedded object). struct ObjectValue { }; /// Create an empty dictionary in-place (does not clear an existing dictionary). struct Dictionary { }; /// Sentinel value for an erased dictionary element. struct Erased { }; /// Payload data types, corresponding loosely to the `DataType` enum in /// Core, but with some special values: /// /// - Null (0) indicates a NULL value of any type. /// - GlobalKey (-1) indicates an internally generated object ID. /// - ObjectValue (-2) indicates the creation of an embedded object. /// - Dictionary (-3) indicates the creation of a dictionary. /// - Erased (-4) indicates that a dictionary element should be erased. /// - Undefined (-5) indicates the /// /// Furthermore, link values for both Link and LinkList columns are /// represented by a single Link type. /// /// Note: For Mixed columns (including typed links), no separate value is required, because the /// instruction set encodes the type of each value in the instruction. enum class Type : int8_t { // Special value indicating that a dictionary element should be erased. Erased = -4, // Special value indicating that a dictionary should be created at the position. Dictionary = -3, // Special value indicating that an embedded object should be created at // the position. ObjectValue = -2, GlobalKey = -1, Null = 0, Int = 1, Bool = 2, String = 3, Binary = 4, Timestamp = 5, Float = 6, Double = 7, Decimal = 8, Link = 9, ObjectId = 10, UUID = 11, }; struct Link { InternString target_table; PrimaryKey target; friend bool operator==(const Link& lhs, const Link& rhs) noexcept { return lhs.target_table == rhs.target_table && lhs.target == rhs.target; } }; union Data { GlobalKey key; int64_t integer; bool boolean; StringBufferRange str; StringBufferRange binary; Timestamp timestamp; float fnum; double dnum; Decimal128 decimal; ObjectId object_id; UUID uuid; Link link; ObjLink typed_link; Data() {} }; Data data; Type type; Payload() : Payload(realm::util::none) { } explicit Payload(bool value) noexcept : type(Type::Bool) { data.boolean = value; } explicit Payload(int64_t value) noexcept : type(Type::Int) { data.integer = value; } explicit Payload(float value) noexcept : type(Type::Float) { data.fnum = value; } explicit Payload(double value) noexcept : type(Type::Double) { data.dnum = value; } explicit Payload(Link value) noexcept : type(Type::Link) { data.link = value; } explicit Payload(StringBufferRange value, bool is_binary = false) noexcept : type(is_binary ? Type::Binary : Type::String) { if (is_binary) { data.binary = value; } else { data.str = value; } } explicit Payload(realm::util::None) noexcept : type(Type::Null) { } // Note: Intentionally implicit. Payload(const ObjectValue&) noexcept : type(Type::ObjectValue) { } // Note: Intentionally implicit. Payload(const Erased&) noexcept : type(Type::Erased) { } explicit Payload(Timestamp value) noexcept : type(value.is_null() ? Type::Null : Type::Timestamp) { if (value.is_null()) { type = Type::Null; } else { type = Type::Timestamp; data.timestamp = value; } } explicit Payload(ObjectId value) noexcept : type(Type::ObjectId) { data.object_id = value; } explicit Payload(Decimal128 value) noexcept { if (value.is_null()) { type = Type::Null; } else { type = Type::Decimal; data.decimal = value; } } explicit Payload(UUID value) noexcept : type(Type::UUID) { data.uuid = value; } Payload(const Payload&) noexcept = default; Payload& operator=(const Payload&) noexcept = default; bool is_null() const noexcept { return type == Type::Null; } friend bool operator==(const Payload& lhs, const Payload& rhs) noexcept { if (lhs.type == rhs.type) { switch (lhs.type) { case Type::Erased: return true; case Type::Dictionary: return lhs.data.key == rhs.data.key; case Type::ObjectValue: return true; case Type::GlobalKey: return lhs.data.key == rhs.data.key; case Type::Null: return true; case Type::Int: return lhs.data.integer == rhs.data.integer; case Type::Bool: return lhs.data.boolean == rhs.data.boolean; case Type::String: return lhs.data.str == rhs.data.str; case Type::Binary: return lhs.data.binary == rhs.data.binary; case Type::Timestamp: return lhs.data.timestamp == rhs.data.timestamp; case Type::Float: return lhs.data.fnum == rhs.data.fnum; case Type::Double: return lhs.data.dnum == rhs.data.dnum; case Type::Decimal: return lhs.data.decimal == rhs.data.decimal; case Type::Link: return lhs.data.link == rhs.data.link; case Type::ObjectId: return lhs.data.object_id == rhs.data.object_id; case Type::UUID: return lhs.data.uuid == rhs.data.uuid; } } return false; } friend bool operator!=(const Payload& lhs, const Payload& rhs) noexcept { return !(lhs == rhs); } }; /// All instructions are TableInstructions. struct TableInstruction { InternString table; protected: bool operator==(const TableInstruction& rhs) const noexcept { return table == rhs.table; } }; /// All instructions except schema instructions are ObjectInstructions. struct ObjectInstruction : TableInstruction { PrimaryKey object; protected: bool operator==(const ObjectInstruction& rhs) const noexcept { return TableInstruction::operator==(rhs) && object == rhs.object; } }; /// All instructions except schema instructions and CreateObject/EraseObject are PathInstructions. struct PathInstruction : ObjectInstruction { InternString field; Path path; uint32_t& index() noexcept { return path.index(); } uint32_t index() const noexcept { return path.index(); } protected: bool operator==(const PathInstruction& rhs) const noexcept { return ObjectInstruction::operator==(rhs) && field == rhs.field && path == rhs.path; } }; struct AddTable : TableInstruction { // Note: Tables "without" a primary key have a secret primary key of type // ObjKey. The field name of such primary keys is assumed to be "_id". struct PrimaryKeySpec { InternString field; Payload::Type type; bool nullable; bool operator==(const PrimaryKeySpec& rhs) const noexcept { return field == rhs.field && type == rhs.type && nullable == rhs.nullable; } }; struct EmbeddedTable { bool operator==(const EmbeddedTable&) const noexcept { return true; } }; mpark::variant type; bool operator==(const AddTable& rhs) const noexcept { return TableInstruction::operator==(rhs) && type == rhs.type; } }; struct EraseTable : TableInstruction { using TableInstruction::TableInstruction; bool operator==(const EraseTable& rhs) const noexcept { return TableInstruction::operator==(rhs); } }; struct AddColumn : TableInstruction { using TableInstruction::TableInstruction; // This is backwards compatible with previous boolean type where 0 // indicated simple type and 1 indicated list. enum class CollectionType : uint8_t { Single, List, Dictionary, Set }; InternString field; // `Type::Null` for Mixed columns. Mixed columns are always nullable. Payload::Type type; // `Type::Null` for other than dictionary columns Payload::Type key_type; bool nullable; // For Mixed columns, this is `none`. Mixed columns are always nullable. // // For dictionaries, this must always be `Type::String`. CollectionType collection_type; InternString link_target_table; bool operator==(const AddColumn& rhs) const noexcept { return TableInstruction::operator==(rhs) && field == rhs.field && type == rhs.type && key_type == rhs.key_type && nullable == rhs.nullable && collection_type == rhs.collection_type && link_target_table == rhs.link_target_table; } }; struct EraseColumn : TableInstruction { using TableInstruction::TableInstruction; InternString field; bool operator==(const EraseColumn& rhs) const noexcept { return TableInstruction::operator==(rhs) && field == rhs.field; } }; struct CreateObject : ObjectInstruction { using ObjectInstruction::ObjectInstruction; bool operator==(const CreateObject& rhs) const noexcept { return ObjectInstruction::operator==(rhs); } }; struct EraseObject : ObjectInstruction { using ObjectInstruction::ObjectInstruction; bool operator==(const EraseObject& rhs) const noexcept { return ObjectInstruction::operator==(rhs); } }; struct Update : PathInstruction { using PathInstruction::PathInstruction; // Note: For "ArrayUpdate", the path ends with an integer. Payload value; union { bool is_default; // For fields uint32_t prior_size; // For "ArrayUpdate" }; Update() : prior_size(0) { } bool is_array_update() const noexcept { return path.is_array_index(); } bool operator==(const Update& rhs) const noexcept { return PathInstruction::operator==(rhs) && value == rhs.value && (is_array_update() ? is_default == rhs.is_default : prior_size == rhs.prior_size); } }; struct AddInteger : PathInstruction { using PathInstruction::PathInstruction; int64_t value; bool operator==(const AddInteger& rhs) const noexcept { return PathInstruction::operator==(rhs) && value == rhs.value; } }; struct ArrayInsert : PathInstruction { // Note: The insertion index is the last path component. using PathInstruction::PathInstruction; Payload value; uint32_t prior_size; bool operator==(const ArrayInsert& rhs) const noexcept { return PathInstruction::operator==(rhs) && value == rhs.value && prior_size == rhs.prior_size; } }; struct ArrayMove : PathInstruction { // Note: The move-from index is the last path component. using PathInstruction::PathInstruction; uint32_t ndx_2; uint32_t prior_size; bool operator==(const ArrayMove& rhs) const noexcept { return PathInstruction::operator==(rhs) && ndx_2 == rhs.ndx_2 && prior_size == rhs.prior_size; } }; struct ArrayErase : PathInstruction { // Note: The erased index is the last path component. using PathInstruction::PathInstruction; uint32_t prior_size; bool operator==(const ArrayErase& rhs) const noexcept { return PathInstruction::operator==(rhs) && prior_size == rhs.prior_size; } }; struct Clear : PathInstruction { using PathInstruction::PathInstruction; bool operator==(const Clear& rhs) const noexcept { return PathInstruction::operator==(rhs); } }; struct SetInsert : PathInstruction { using PathInstruction::PathInstruction; Payload value; bool operator==(const SetInsert& rhs) const noexcept { return PathInstruction::operator==(rhs) && value == rhs.value; } }; struct SetErase : PathInstruction { using PathInstruction::PathInstruction; Payload value; bool operator==(const SetErase& rhs) const noexcept { return PathInstruction::operator==(rhs) && value == rhs.value; } }; } // namespace instr struct Instruction { #define REALM_DECLARE_INSTRUCTION_STRUCT(X) using X = instr::X; REALM_FOR_EACH_INSTRUCTION_TYPE(REALM_DECLARE_INSTRUCTION_STRUCT) #undef REALM_DECLARE_INSTRUCTION_STRUCT using TableInstruction = instr::TableInstruction; using ObjectInstruction = instr::ObjectInstruction; using PathInstruction = instr::PathInstruction; using PrimaryKey = instr::PrimaryKey; using Payload = instr::Payload; using Path = instr::Path; using Vector = std::vector; // CAUTION: Any change to the enum values for the instruction types is a protocol-breaking // change! enum class Type : uint8_t { AddTable = 0, EraseTable = 1, CreateObject = 2, EraseObject = 3, Update = 4, // Note: Also covers ArrayUpdate AddInteger = 5, AddColumn = 6, EraseColumn = 7, ArrayInsert = 8, ArrayMove = 9, ArrayErase = 10, Clear = 11, SetInsert = 12, SetErase = 13, }; template struct GetType; template struct GetInstructionType; template Instruction(T instr); mpark::variant m_instr; Type type() const noexcept; template decltype(auto) visit(F&& lambda); template decltype(auto) visit(F&& lambda) const; template T* get_if() noexcept; template const T* get_if() const noexcept { return const_cast(*this).get_if(); } template T& get_as() { auto ptr = get_if(); REALM_ASSERT(ptr); return *ptr; } template const T& get_as() const { auto ptr = get_if(); REALM_ASSERT(ptr); return *ptr; } bool operator==(const Instruction& other) const noexcept; bool operator!=(const Instruction& other) const noexcept { return !(*this == other); } bool is_vector() const noexcept { return mpark::holds_alternative(m_instr); } size_t path_length() const noexcept; Vector& convert_to_vector(); void insert(size_t pos, Instruction instr); void erase(size_t pos); size_t size() const noexcept; bool is_empty() const noexcept; Instruction& at(size_t) noexcept; const Instruction& at(size_t) const noexcept; private: template struct Visitor; }; inline const char* get_type_name(Instruction::Type type) { switch (type) { #define REALM_INSTRUCTION_TYPE_TO_STRING(X) \ case Instruction::Type::X: \ return #X; REALM_FOR_EACH_INSTRUCTION_TYPE(REALM_INSTRUCTION_TYPE_TO_STRING) #undef REALM_INSTRUCTION_TYPE_TO_STRING } return "(invalid)"; } inline std::ostream& operator<<(std::ostream& os, Instruction::Type type) { return os << get_type_name(type); } inline const char* get_type_name(Instruction::Payload::Type type) { using Type = Instruction::Payload::Type; switch (type) { case Type::Erased: return "Erased"; case Type::Dictionary: return "Dictionary"; case Type::ObjectValue: return "ObjectValue"; case Type::GlobalKey: return "GlobalKey"; case Type::Null: return "Null"; case Type::Int: return "Int"; case Type::Bool: return "Bool"; case Type::String: return "String"; case Type::Binary: return "Binary"; case Type::Timestamp: return "Timestamp"; case Type::Float: return "Float"; case Type::Double: return "Double"; case Type::Decimal: return "Decimal"; case Type::Link: return "Link"; case Type::ObjectId: return "ObjectId"; case Type::UUID: return "UUID"; } return "(unknown)"; } inline const char* get_collection_type(Instruction::AddColumn::CollectionType type) { using Type = Instruction::AddColumn::CollectionType; switch (type) { case Type::Single: return "Single"; case Type::List: return "List"; case Type::Dictionary: return "Dictionary"; case Type::Set: return "Set"; } return "(unknown)"; } inline const char* get_type_name(util::Optional type) { if (type) { return get_type_name(*type); } else { return "Mixed"; } } inline std::ostream& operator<<(std::ostream& os, Instruction::Payload::Type type) { return os << get_type_name(type); } inline bool is_valid_key_type(Instruction::Payload::Type type) noexcept { using Type = Instruction::Payload::Type; switch (type) { case Type::Int: [[fallthrough]]; case Type::String: [[fallthrough]]; case Type::ObjectId: [[fallthrough]]; case Type::UUID: [[fallthrough]]; case Type::GlobalKey: return true; case Type::Null: // Mixed is not a valid primary key [[fallthrough]]; default: return false; } } inline DataType get_data_type(Instruction::Payload::Type type) noexcept { using Type = Instruction::Payload::Type; switch (type) { case Type::Int: return type_Int; case Type::Bool: return type_Bool; case Type::String: return type_String; case Type::Binary: return type_Binary; case Type::Timestamp: return type_Timestamp; case Type::Float: return type_Float; case Type::Double: return type_Double; case Type::Decimal: return type_Decimal; case Type::Link: return type_Link; case Type::ObjectId: return type_ObjectId; case Type::UUID: return type_UUID; case Type::Null: // Mixed is encoded as null return type_Mixed; case Type::Erased: [[fallthrough]]; case Type::Dictionary: [[fallthrough]]; case Type::ObjectValue: [[fallthrough]]; case Type::GlobalKey: REALM_TERMINATE(util::format("Invalid data type: %1", int8_t(type)).c_str()); } return type_Int; // Make compiler happy } // 0x3f is the largest value that fits in a single byte in the variable-length // encoded integer instruction format. static constexpr uint8_t InstrTypeInternString = 0x3f; // This instruction code is only ever used internally by the Changeset class // to allow insertion/removal while keeping iterators stable. Should never // make it onto the wire. static constexpr uint8_t InstrTypeMultiInstruction = 0xff; struct InstructionHandler { /// Notify the handler that an InternString meta-instruction was found. virtual void set_intern_string(uint32_t index, StringBufferRange) = 0; /// Notify the handler of the string value. The handler guarantees that the /// returned string range is valid at least until the next invocation of /// add_string_range(). /// /// Instances of `StringBufferRange` passed to operator() after invoking /// this function are assumed to refer to ranges in this buffer. virtual StringBufferRange add_string_range(StringData) = 0; /// Handle an instruction. virtual void operator()(const Instruction&) = 0; }; /// Implementation: #define REALM_DEFINE_INSTRUCTION_GET_TYPE(X) \ template <> \ struct Instruction::GetType { \ using Type = Instruction::X; \ }; \ template <> \ struct Instruction::GetInstructionType { \ static const Instruction::Type value = Instruction::Type::X; \ }; REALM_FOR_EACH_INSTRUCTION_TYPE(REALM_DEFINE_INSTRUCTION_GET_TYPE) #undef REALM_DEFINE_INSTRUCTION_GET_TYPE template Instruction::Instruction(T instr) : m_instr(std::move(instr)) { static_assert(!std::is_same_v); } template struct Instruction::Visitor { F lambda; // reference type Visitor(F lambda) : lambda(lambda) { } template decltype(auto) operator()(T& instr) { return lambda(instr); } template decltype(auto) operator()(const T& instr) { return lambda(instr); } auto operator()(const Instruction::Vector&) -> decltype(lambda(std::declval())) { REALM_TERMINATE("visiting instruction vector"); } auto operator()(Instruction::Vector&) -> decltype(lambda(std::declval())) { REALM_TERMINATE("visiting instruction vector"); } }; template inline decltype(auto) Instruction::visit(F&& lambda) { // Cannot use std::visit, because it does not pass lvalue references to the visitor. if (mpark::holds_alternative(m_instr)) { REALM_TERMINATE("visiting instruction vector"); } #define REALM_VISIT_VARIANT(X) \ else if (mpark::holds_alternative(m_instr)) \ { \ return lambda(mpark::get(m_instr)); \ } REALM_FOR_EACH_INSTRUCTION_TYPE(REALM_VISIT_VARIANT) #undef REALM_VISIT_VARIANT else { REALM_TERMINATE("Unhandled instruction variant entry"); } } template inline decltype(auto) Instruction::visit(F&& lambda) const { // Cannot use std::visit, because it does not pass lvalue references to the visitor. if (mpark::holds_alternative(m_instr)) { REALM_TERMINATE("visiting instruction vector"); } #define REALM_VISIT_VARIANT(X) \ else if (mpark::holds_alternative(m_instr)) \ { \ return lambda(mpark::get(m_instr)); \ } REALM_FOR_EACH_INSTRUCTION_TYPE(REALM_VISIT_VARIANT) #undef REALM_VISIT_VARIANT else { REALM_TERMINATE("Unhandled instruction variant entry"); } } inline Instruction::Type Instruction::type() const noexcept { return visit([](auto&& instr) { using T = std::remove_cv_t>; return GetInstructionType::value; }); } inline bool Instruction::operator==(const Instruction& other) const noexcept { return m_instr == other.m_instr; } template REALM_NOINLINE T* Instruction::get_if() noexcept { // FIXME: Is there a way to express this without giant switch statements? Note: Putting the // base class into a union does not seem to be allowed by the standard. if constexpr (std::is_same_v) { // This should compile to nothing but a comparison of the type. return visit([](auto& instr) -> TableInstruction* { return &instr; }); } else if constexpr (std::is_same_v) { // This should compile to nothing but a comparison of the type. return visit(util::overload{ [](AddTable&) -> ObjectInstruction* { return nullptr; }, [](EraseTable&) -> ObjectInstruction* { return nullptr; }, [](AddColumn&) -> ObjectInstruction* { return nullptr; }, [](EraseColumn&) -> ObjectInstruction* { return nullptr; }, [](auto& instr) -> ObjectInstruction* { return &instr; }, }); } else if constexpr (std::is_same_v) { // This should compile to nothing but a comparison of the type. return visit(util::overload{ [](AddTable&) -> PathInstruction* { return nullptr; }, [](EraseTable&) -> PathInstruction* { return nullptr; }, [](AddColumn&) -> PathInstruction* { return nullptr; }, [](EraseColumn&) -> PathInstruction* { return nullptr; }, [](CreateObject&) -> PathInstruction* { return nullptr; }, [](EraseObject&) -> PathInstruction* { return nullptr; }, [](auto& instr) -> PathInstruction* { return &instr; }, }); } else { return mpark::get_if(&m_instr); } } inline size_t Instruction::size() const noexcept { if (auto vec = mpark::get_if(&m_instr)) { return vec->size(); } return 1; } inline bool Instruction::is_empty() const noexcept { return size() == 0; } inline Instruction& Instruction::at(size_t idx) noexcept { if (auto vec = mpark::get_if(&m_instr)) { REALM_ASSERT(idx < vec->size()); return (*vec)[idx]; } REALM_ASSERT(idx == 0); return *this; } inline const Instruction& Instruction::at(size_t idx) const noexcept { if (auto vec = mpark::get_if(&m_instr)) { REALM_ASSERT(idx < vec->size()); return (*vec)[idx]; } REALM_ASSERT(idx == 0); return *this; } inline size_t Instruction::path_length() const noexcept { // Find the path length of the instruction. This affects how OT decides // which instructions are potentially nesting. // // AddTable/EraseTable: Length 1 // AddColumn/EraseColumn: Length 2 (table, field) // Object instructions: Length 2 (table, object) // Path instructions: Length 3 + m_path.size (table, object, field, path...) if (auto path_instr = get_if()) { return 3 + path_instr->path.size(); } if (get_if()) { return 2; } switch (type()) { case Instruction::Type::AddColumn: [[fallthrough]]; case Instruction::Type::EraseColumn: { return 2; } case Instruction::Type::AddTable: [[fallthrough]]; case Instruction::Type::EraseTable: { return 1; } default: REALM_TERMINATE("Unhandled instruction type in Instruction::path_len()"); } } inline Instruction::Vector& Instruction::convert_to_vector() { if (auto v = mpark::get_if(&m_instr)) { return *v; } else { Vector vec; vec.emplace_back(std::move(*this)); m_instr = std::move(vec); return mpark::get(m_instr); } } inline void Instruction::insert(size_t idx, Instruction instr) { auto& vec = convert_to_vector(); REALM_ASSERT(idx <= vec.size()); vec.emplace(vec.begin() + idx, std::move(instr)); } inline void Instruction::erase(size_t idx) { auto& vec = convert_to_vector(); REALM_ASSERT(idx < vec.size()); vec.erase(vec.begin() + idx); } } // namespace sync } // namespace realm #endif // REALM_IMPL_INSTRUCTIONS_HPP