OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-atomspace / opencog / atoms / base / Atom.h

729 lines · c ref:
/*
 * opencog/atoms/base/Atom.h
 *
 * Copyright (C) 2002-2007 Novamente LLC
 * All Rights Reserved
 *
 * Written by Thiago Maia <thiago@vettatech.com>
 *            Andre Senna <senna@vettalabs.com>
 *            Welter Silva <welter@vettalabs.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License v3 as
 * published by the Free Software Foundation and including the exceptions
 * at http://opencog.org/wiki/Licenses
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 */

#ifndef _OPENCOG_ATOM_H
#define _OPENCOG_ATOM_H

#include <atomic>
#include <functional>
#include <memory>
#include <shared_mutex>
#include <string>
#include <unordered_set>

#if HAVE_SPARSEHASH
#include <sparsehash/sparse_hash_set>
#include <sparsehash/sparse_hash_map>
#define USE_HASHABLE_WEAK_PTR 1
#endif

#if HAVE_FOLLY
#include <folly/container/F14Set.h>
#define USE_HASHABLE_WEAK_PTR 1
#endif

#include <opencog/util/exceptions.h>

#include <opencog/atoms/base/Handle.h>
#include <opencog/atoms/value/Value.h>
#include <opencog/atoms/value/BoolValue.h>

namespace opencog
{
#if USE_HASHABLE_WEAK_PTR
template<class T>
struct hashable_weak_ptr : public std::weak_ptr<T>
{
	hashable_weak_ptr(std::shared_ptr<T>const& sp) :
		std::weak_ptr<T>(sp)
	{
		if (!sp) return;
		_hash = std::hash<T*>{}(sp.get());
	}

#if HAVE_SPARSEHASH
	// google::sparse_hash_set uses a zero-sized ctor to indicate
	// the deleted key. So provide that, setting it to zero.
	static std::weak_ptr<T> _dummy;
	hashable_weak_ptr(void) :
		std::weak_ptr<T>(_dummy)
	{
		_hash = 0;
	}
#endif

	std::size_t get_hash() const noexcept { return _hash; }

	// Define operator<() in order to construct operator==()
	// It might be more efficient to store the unhashed
	// pointer, and use that for equality compares...
	friend bool operator<(hashable_weak_ptr const& lhs, hashable_weak_ptr const& rhs)
	{
		return lhs.owner_before(rhs);
	}
	friend bool operator!=(hashable_weak_ptr const& lhs, hashable_weak_ptr const& rhs)
	{
		return lhs<rhs or rhs<lhs;
	}
	friend bool operator==(hashable_weak_ptr const& lhs, hashable_weak_ptr const& rhs)
	{
		return not (lhs != rhs);
	}
	private:
		std::size_t _hash = 0;
};

typedef hashable_weak_ptr<Atom> WinkPtr;

#else // USE_HASHABLE_WEAK_PTR

// See discussion in README, explaining why using a bare pointer is safe.
//
// Based on recent measurements (March 2022, benchmark/query-loop/diary.txt)
// the use of bare pointers does not affect performance. There is a
// savings of 31 bytes per Atom, as measured against a mid-size dataset.
// Disabled, for now, out of general paranoia.
// #define USE_BARE_BACKPOINTER 1
//
#if USE_BARE_BACKPOINTER
typedef const Atom* WinkPtr;
#else // USE_BARE_BACKPOINTER
typedef std::weak_ptr<Atom> WinkPtr;
#endif // USE_BARE_BACKPOINTER

#endif // USE_HASHABLE_WEAK_PTR
}

#if USE_BARE_BACKPOINTER
	#define WEAKLY_DO(HA,WP,STMT) { Handle HA(WP->get_handle()); STMT; }
#else  // USE_BARE_BACKPOINTER
	#define WEAKLY_DO(HA,WP,STMT) { Handle HA(WP.lock()); if (HA) { STMT; }}
#endif // USE_BARE_BACKPOINTER

namespace std
{

#if USE_HASHABLE_WEAK_PTR
template<class T> struct owner_less<opencog::hashable_weak_ptr<T>>
{
	bool operator()(const opencog::hashable_weak_ptr<T>& lhs,
	                const opencog::hashable_weak_ptr<T>& rhs) const noexcept
	{
		return lhs.owner_before(rhs);
	}
};

template<class T> struct hash<opencog::hashable_weak_ptr<T>>
{
	std::size_t operator()(const opencog::hashable_weak_ptr<T>& w) const noexcept
	{
		return w.get_hash();
	}
};
#else // USE_HASHABLE_WEAK_PTR

#if USE_BARE_BACKPOINTER
template <> struct owner_less<const opencog::Atom*>
{
	bool operator()(const opencog::Atom* const& lhs,
	                const opencog::Atom* const& rhs) const noexcept
	{
		return lhs < rhs;
	}
};
#endif //USE_BARE_BACKPOINTER

#endif // USE_HASHABLE_WEAK_PTR


} // namespace std

namespace opencog
{

/** \addtogroup grp_atomspace
 *  @{
 */

class AtomSpace;

//! arity of Links, represented as size_t to match outcoming set limit
typedef std::size_t Arity;

//! We use a std:vector instead of std::set for IncomingSet, because
//! virtually all access will be either insert, or iterate, so we get
//! O(1) performance. Note that sometimes incoming sets can be huge,
//! millions of atoms.
typedef HandleSeq IncomingSet;

// ----------------------------------------------------
// Games with the structures used for the Incoming set.
// The size of Atoms, and performance depends on these.

// Hard-code the flag here. This is less flexible than doing it with
// CMakefiles, but its also less complicated and less error-prone.
#if HAVE_SPARSEHASH
	#define USE_SPARSE_INCOMING 1
#endif

#if USE_SPARSE_INCOMING
typedef google::sparse_hash_set<WinkPtr> WincomingSet;
#endif

#if USE_FOLLY
// typedef folly::F14ValueSet<WinkPtr, std::owner_hash<WinkPtr> > WincomingSet;
typedef folly::F14ValueSet<WinkPtr> WincomingSet;
#endif

#if not (USE_SPARSE_INCOMING || USE_FOLLY)
// typedef std::unordered_set<WinkPtr> WincomingSet;
typedef std::set<WinkPtr, std::owner_less<WinkPtr> > WincomingSet;
#endif

// ----------------------------------------------------
// Other maps.
typedef std::map<Type, WincomingSet> InSetMap;

#if USE_SPARSE_KVP
typedef google::sparse_hash_map<Handle, ValuePtr> KVPMap;

// USE_SPARSE_KVP works fine. However, for typical datasets, it uses
// more memory. Why? Because almost all Atoms will have zero or one
// Values on them. See immediately below for a detailed report.
// Anyway, we disable, to avoid screw-ups.
#error "USE_SPARSE_KVP is enabled! It works, but you probably did this by accident. If you meant to do this, edit the header file and try again."
#else
typedef std::map<const Handle, ValuePtr> KVPMap;
#endif

/**
 * Atoms are the basic implementational unit in the system that
 * represents nodes and links. In terms of C++ inheritance, Nodes and
 * Links are specializations of Atoms, that is, they inherit all
 * properties from Atoms.
 *
 * A "typical" atom in a small AtomSpace is about 500 Bytes in size.
 * Large AtomSpaces (with millions of Atoms) have sizes of 750 Bytes
 * up to 2KBytes, with about 1.3KB being "typical". The vairance is due
 * incoming sets and attached Values.
 *
 * The RAM usage breakdown is as follows:
 * -- 24 Bytes std::enable_shared_from_this<Value>
 * --  4 Bytes Type _type plus 4 bool flags.
 * --  8 Bytes ContentHash _content_hash;
 * --  8 Bytes AtomSpace *_atom_space;
 * -- 48 Bytes std::map<const Handle, ValuePtr> _values;
 * -- 48 Bytes std::map<Type, WincomingSet> _incoming_set;
 * Total: 144 Bytes for a base naked Atom.
 *
 * Node: Additional 32 Bytes for std::string _name + sizeof(chars of string)
 * Link: Additional 24 Bytes for std::vector _outgoing + 16*(_outgoing.size());
 *       A "typical" Link of size 2 is 200 Bytes, outside of AtomSpace
 *
 * Inserted into the AtomSpace: ?? per hash bucket. I guess 24 or 32
 * Per addition to incoming set: 64 per std::_Rb_tree node
 * With a value of three doubles, e.g. FloatValue:
 * -- 24 Bytes std::enable_shared_from_this<Value>
 * --  8 Bytes Type _type plus padding
 * -- 24 Bytes std::vector<double> _value
 * -- 24 Bytes 3*sizeof(double)
 * -- 64 std::_Rb_tree node in the Atom holding the TV
 * Total: 144 Bytes per FloatValue (ouch).
 *
 * A "typical" Link of size 2, held in one other Link, in AtomSpace,
 *   holding a FloatValue in it: 344 Bytes. With a large incomnig set,
 *   this expands to 750 Byte to 2KByte range. This is indeed what is
 *   measured in real-life large datasets.
 *
 * How to use less memory:
 * Measured on a dataset w/ 5.5 million Atoms (the sfia pairs dataset)
 * -- Using the MutexPool shrinks Atom size by 63 bytes/atom.
 *    It also runs slightly faster, probably a cache effect.
 * -- std::set<Atom*> vs std::set<WinkPtr> saves 31 bytes/atom.
 *    enable USE_BARE_BACKPOINTER to get this.
 * -- sparse_hash_set<WinkPtr> saves 25 bytes/atom.
 * -- sparse_hash_set<Atom*> is same size as WinkPtr so it is
 *    actually larger than std::set<Atom*>
 * -- Enabling USE_SPARSE_KVP makes things worse for the sfia dataset.
 *    Why? Because half the Atoms have zero Values on them, and the
 *    other half have only one. It's hard/impossible to improve on this.
 *    BTW, it also runs 10% slower just to load the dataset. This is
 *    due to a more complex init than what std::map has to do.
 * -- Enabling USE_INCOME_INDEX provides no savings at all, and runs
 *    slower. The original ideas was this: about half of all Atoms do
 *    not have an incoming set. Since sizeof(InSetMap) = 48, we can
 *    save 48 bytes by moving it out of the Atom, and to a hashtable.
 *    The problem is that each hashtable entry takes up 24+24=48 bytes;
 *    24 for the Handle used in the lookup, and 24 for the std::pair<>
 *    So, half as many entries, but twice the size per entry.
 * -- For the same reason, moving Values out-of-line won't work: the
 *    decrease in the size of the Atom is outweighed by the increase
 *    in the references to the out-of-line storage.
 */
class Atom
    : public Value
{
    friend class AtomSpace;       // Needs to setChecked
    friend class TypeIndex;       // Needs to clear _atom_space
    friend class Link;            // Needs to call install_atom()
    friend class Frame;           // Needs to call install_atom()
    friend class StateLink;       // Needs to call swap_atom()
    friend class StorageNode;     // Needs to call isAbsent()
    template <typename T> friend class ObjectCRTP; // Needs to call markIsMessage()

protected:

#define USE_MUTEX_POOL 1
#if USE_MUTEX_POOL
    // The MutexPool saves 64 bytes per Atom, by avoiding having a
    // a mutex in-line in the Atom. As long as the pool is 1x to 4x
    // larger than the number of CPU's on the machine, the chances
    // of collision will be small, and contention smaller still.
    // The biggest stumbling block to high parallelism is not this,
    // but the std::shared_ptr<>, which uses atomics that use
    // CPU-dependent cache-line proprietary unpublished voodoo,
    // with different CPU's, even from the same family, having
    // radically different cache microarches. But I digress...
    //
    // CPU usage improves 3% to 6%, probably because the shrinkage
    // fits into the cache better.
    struct MutexPool
    {
        static constexpr size_t POOL_SIZE = 64;
        mutable std::shared_mutex mutexes[POOL_SIZE];
        inline std::shared_mutex& get_mutex(ContentHash hsh) {
            return mutexes[hsh % POOL_SIZE];
        }
    };
    static MutexPool _mutex_pool;

    #define _MTX (_mutex_pool.get_mutex(get_hash()))
#else
    #define _MTX _mtx
#endif
    #define INCOMING_SHARED_LOCK std::shared_lock<std::shared_mutex> lck(_MTX);
    #define INCOMING_UNIQUE_LOCK std::unique_lock<std::shared_mutex> lck(_MTX);
    #define KVP_UNIQUE_LOCK std::unique_lock<std::shared_mutex> lck(_MTX);
    #define KVP_SHARED_LOCK std::shared_lock<std::shared_mutex> lck(_MTX);

    // Packed flas. Single byte per atom.
    enum AtomFlags : uint8_t {
        ABSENT_FLAG     = 0x01,  // 0000 0001
        MARKED_FLAG     = 0x02,  // 0000 0010
        CHECKED_FLAG    = 0x04,  // 0000 0100
        USE_ISET_FLAG   = 0x08,  // 0000 1000
        IS_KEY_FLAG     = 0x10,  // 0001 0000
        IS_MESSAGE_FLAG = 0x20   // 0010 0000
    };
    mutable std::atomic<uint8_t> _flags;

    /// Merkle-tree hash of the atom contents. Generically useful
    /// for indexing and comparison operations.
    mutable ContentHash _content_hash;

    //! Sets the AtomSpace in which this Atom is inserted.
    virtual void setAtomSpace(AtomSpace *);

    AtomSpace *_atom_space;

    /// All of the values on the atom, including the TV.
    mutable KVPMap _values;

#if not defined(USE_MUTEX_POOL) or (0 == USE_MUTEX_POOL)
    // Lock, used to serialize changes.
    // This costs 56 bytes per atom.  Tried using a single, global lock,
    // but there seemed to be too much contention for it, so instead,
    // we are using a lock-per-atom, even though this makes the atom
    // fatter.
    mutable std::shared_mutex _mtx;

    // copyValues() assumes it can use the same mutex to lock both the
    // source and destination atoms with the same mutex. That would need
    // to be fixed to use this. And I'm lazy so lets halt compilation.
    #error "Current implementation of copyValues() assumes mutex pool!"
#endif // NOT USE_MUEX_POOL

    /**
     * Constructor for this class. Protected; no user should call this
     * directly.  Only derived classes (Node, Link) can call this.
     *
     * @param The type of the atom.
     */
    Atom(Type t)
      : Value(t),
        _flags(0),
        _content_hash(Handle::INVALID_HASH),
        _atom_space(nullptr)
    {
#if USE_SPARSE_KVP
        _values.set_deleted_key(Handle());
#endif
    }

    Atom& operator=(const Atom& other) // copy assignment operator
        { return *this; }
    Atom& operator=(Atom&& other) // move assignment operator
        { return *this; }

// If USE_INCOME_INDEX is set to true, then the incoming set is moved
// out of line. Turns out the inline version here is smaller, better,
// faster than the out-of-line version.
#ifndef USE_INCOME_INDEX
private:
    // The incoming set cannot be tracked with smart pointers; this is
    // to avoid cyclic references. Instead, weak pointers are used.
    // std::set<ptr> uses 48 bytes (per atom).  See the README file
    // in this directory for a slightly longer explanation for why
    // weak pointers are needed, and why bdwgc cannot be used.
    struct InSet
    {
        // We want five things:
        // a) the smallest possiblem atom.
        // b) excellent insert performance.
        // c) very fast lookup by type.
        // d) good remove performance.
        // e) uniqueness, because user code can insert and remove the
        //    same atom, repeatedly, in different threads.
        //
        // In order to get b), we have to store atoms in buckets, each
        // bucket holding only one type.  To satisfy d), the buckets
        // need to be either hash tables or rb-trees. Scanning for
        // uniqueness in a vector is prohibitavely slow.  Note that
        // incoming sets containing 10K atoms are not unusual, and can
        // be the source of bottlenecks.  Note that an atomspace can
        // contain a hundred-million atoms, so the solution has to be
        // small. This rules out using a vector to store the
        // buckets (I tried).
        // std::map<Type, WincomingSet> _iset;
        InSetMap _iset;
    };
    InSet _local_incoming_set;

protected:
    inline bool have_inset_map(void) const { return true; }
    inline InSetMap& get_inset_map(void) { return _local_incoming_set._iset; }
    inline const InSetMap& get_inset_map_const(void) const { return _local_incoming_set._iset; }
    inline void drop_inset_map(void) {}
#else
    bool have_inset_map(void) const;
    InSetMap& get_inset_map(void);
    const InSetMap& get_inset_map_const(void) const;
    void drop_inset_map(void);
#endif

    void keep_incoming_set();
    void drop_incoming_set();

    // Insert and remove links from the incoming set.
    void insert_atom(const Handle&);
    void remove_atom(const Handle&);
    void swap_atom(const Handle&, const Handle&);
    virtual void install();
    virtual void remove();

protected:
    virtual ContentHash compute_hash() const = 0;

    /** Indicate this Atom is used as a message */
    void markIsMessage();

private:
    //! Returns whether this atom is marked for removal.
    bool isMarkedForRemoval() const;

    //! Marks the atom for removal. Returns old value.
    bool markForRemoval();

    //! Unsets removal flag. Returns old value.
    bool unsetRemovalFlag();

    /** Returns whether this atom is marked checked. */
    bool isChecked() const;
    bool setChecked();
    bool setUnchecked();

    /** Returns whether this atom is marked absent. */
    bool isAbsent() const;
    bool setAbsent();
    bool setPresent();

    /** Indicate this Atom is used as a key */
    void markIsKey();

    void getLocalInc(const AtomSpace*, HandleSet&, Type) const;
    void getCoveredInc(const AtomSpace*, HandleSet&, Type) const;

public:

    virtual ~Atom();
    virtual bool is_atom() const { return true; }

    //! Returns the AtomSpace in which this Atom is inserted.
    AtomSpace* getAtomSpace() const { return _atom_space; }

    /// Merkle-tree hash of the atom contents. Generically useful
    /// for indexing and comparison operations.
    ///
    /// At this time, we use a 64-bit non-cryptographic hash: it is
    /// just enough to disambiguate most Atoms, but is small enough
    /// that it does not use up a lot of RAM (as AtomSpace algos are
    /// RAM-limited, in practice.)
    ///
    /// If a crypto hash was ever needed, the IPLD hash format would
    /// be recommended.  See https://ipld.io/ for details.
    ///
    /// This hash is NOT stable against modifications of the type
    /// inheritance hierarchy! Changing the type hierarchy might
    /// change the hash, because it will assign a different numeric
    /// value to to the type, which is used in computing the hash.
    /// (this could be avoided by using the string name of the type.)
    ///
    /// This hash is NOT stable against different-sized address spaces,
    /// or even different C++ libraries! That is because it uses the
    /// `std::hash()` function to compute string hashes, and this gives
    /// different answers on 32-bit and 64-bit arches. This could be
    /// fixed by using our own, private string hash function.
    ///
    /// It might be nice to have a hash that is stable against both of
    /// these changes, as it would then enable the comparison of hashes
    /// in a distributed atomspace network. But no one needs this, for
    /// now. (There's no code that sends hashes over the network, or
    /// stores them in a file.)
    inline ContentHash get_hash() const {
        if (Handle::INVALID_HASH != _content_hash)
            return _content_hash;
        _content_hash = compute_hash();
        return _content_hash;
    }

    virtual const std::string& get_name() const {
        throw RuntimeException(TRACE_INFO, "Not a node!");
    }

    inline size_t get_arity() const { return size(); }

    virtual const HandleSeq& getOutgoingSet() const {
        throw RuntimeException(TRACE_INFO, "Not a link!");
    }

    virtual Handle getOutgoingAtom(size_t) const {
        throw RuntimeException(TRACE_INFO, "Not a link!");
    }

    virtual bool bevaluate(AtomSpace*, bool silent=false) {
        throw RuntimeException(TRACE_INFO, "Not evaluatable!");
    }
    virtual bool bevaluate(void) { return bevaluate(_atom_space, false); }

    // Non-crisp evaluation is deprecated. Changed to return BoolValue.
    virtual ValuePtr evaluate(AtomSpace* as, bool silent=false) {
        return ValueCast(createBoolValue(bevaluate(as, silent)));
    }
    virtual bool is_evaluatable() const { return false; }

    virtual ValuePtr execute(AtomSpace*, bool silent=false) {
        return std::dynamic_pointer_cast<Atom>(shared_from_this());
    }
    virtual ValuePtr execute(void) { return execute(_atom_space, false); }
    virtual bool is_executable() const { return false; }

    /** Returns a handle holding "this". */
    inline Handle get_handle() const {
        return Handle(std::dynamic_pointer_cast<Atom>(
             const_cast<Atom*>(this)->shared_from_this()));
    }

    // ---------------------------------------------------
    // The setValue() and getValue() methods are virtual, because
    // ObjectNodes use them to intercept messages being sent to the
    // object.

    /// Associate `value` to `key` for this atom.
    virtual void setValue(const Handle& key, const ValuePtr& value);
    /// Get value at `key` for this atom.
    virtual ValuePtr getValue(const Handle& key) const;
    /// Atomically increment a generic FloatValue.
    ValuePtr incrementCount(const Handle& key, const std::vector<double>&);
    ValuePtr incrementCount(const Handle& key, size_t idx, double);

    /// Return true if this Atom is used as a key, somewhere, anywhere.
    bool isKey() const { return _flags.load() & IS_KEY_FLAG; }

    /// Return true if this Atom is used as a message, somewhere, anywhere.
    bool isMessage() const { return _flags.load() & IS_MESSAGE_FLAG; }

    /// Get the set of all keys in use for this Atom.
    HandleSet getKeys() const;

    /// Get the set of all object messages supported by this Atom.
    virtual HandleSeq getMessages() const { return HandleSeq(); }

    /// Return true if handle appears in the list of getMessages().
    virtual bool usesMessage(const Handle&) const { return false; }

    /// Copy all the values from the other atom to this one.
    /// It is thread-safe but racey. Values are copied one at a time.
    /// Multiple updaters in multiple threads can race with one another,
    /// and alter Values here, or on the other Atom, even as the copy
    /// is proceeding. The only thread-safety guarantee here is to not
    /// crash and to not corrupt internal data structures.
    void copyValues(const Handle&);

    /// Lock-free, thread-unsafe copy of all the values from the
    /// other atom to this one, assuming this Atom if fresh, empty,
    /// and not visible from any other threads.
    void bulkCopyValues(const Handle&);

    /// Return true if the set of values on this atom isn't empty.
    bool haveValues() const {
        // I think its safe to call empty() without holding a lock...!?
        // But I'm paranoid. So... grab a lock.
        KVP_SHARED_LOCK
        return not _values.empty();
    }

    /// Print all of the key-value pairs.
    std::string valuesToString() const;

    /// Remove all values. The only anticipated users of this are the
    // storage backends, manipulating multi-AtomSpace bulk loads.
    void clearValues();

    // ---------------------------------------------------
    //! Return true if the incoming set is empty.
    bool isIncomingSetEmpty(const AtomSpace* = nullptr) const;

    //! Get the size of the incoming set.
    size_t getIncomingSetSize(const AtomSpace* = nullptr) const;

    //! Return the incoming set of this atom.
    //! If the AtomSpace pointer is non-null, then only those atoms
    //! that belonged to that atomspace at the time this call was made
    //! are returned. Otherwise, the entire incoming set is returned.
    //!
    //! This call is thread-safe against simultaneous deletion of atoms.
    //! That is, this call returns the incoming set as it was at the
    //! time of the call; any deletions that occur afterwards (possibly
    //! in other threads) will not be reflected in the returned set.
    IncomingSet getIncomingSet(const AtomSpace* = nullptr) const;

    /** Functional version of getIncomingSetByType.  */
    IncomingSet getIncomingSetByType(Type, const AtomSpace* = nullptr) const;

    /** Return the size of the incoming set, for the given type. */
    size_t getIncomingSetSizeByType(Type, const AtomSpace* = nullptr) const;

    // ---------------------------------------------------
    /** Returns a string representation of the node. */
    virtual std::string to_string(const std::string& indent) const = 0;
    virtual std::string to_short_string(const std::string& indent) const = 0;
    virtual std::string id_to_string() const;

    // Work around gdb's inability to build a string on the fly,
    // see http://stackoverflow.com/questions/16734783 for more
    // explanation.
    std::string to_string() const { return to_string(""); }
    std::string to_short_string() const { return to_short_string(""); }

    /**
     * Perform a content-based comparison of two atoms.
     * Returns true if the other atom is "semantically" equivalent
     * to this one. Two atoms are semantically equivalent if they
     * accomplish the same thing; even if they differ in details.
     * For example, two ScopeLinks can be semantically equivalent,
     * even though they use different variable names. As long as
     * the different names can be alpha-converted, two different
     * declarations are "the same", and count as the same atom.
     *
     * @return true if the atoms are semantically equal, else false.
     */
    virtual bool operator==(const Atom&) const = 0;

    /** Negation of operator==(). */
    bool operator!=(const Atom& other) const
    { return not operator==(other); }

    virtual bool operator==(const Value& other) const
    {
        if (_type != other.get_type()) return false;
        return operator==(dynamic_cast<const Atom&>(other));
    }

    /** Ordering operator for Atoms. */
    using Value::operator<;  // Bring base class operator< into scope
    virtual bool operator<(const Atom&) const = 0;
};

#define ATOM_PTR_DECL(CNAME)                                \
    typedef std::shared_ptr<CNAME> CNAME##Ptr;              \
    static inline CNAME##Ptr CNAME##Cast(const Handle& h)   \
        { return std::dynamic_pointer_cast<CNAME>(h); }     \
    static inline CNAME##Ptr CNAME##Cast(const ValuePtr& v) \
        { return std::dynamic_pointer_cast<CNAME>(v); }

#define CREATE_DECL(CNAME)  std::make_shared<CNAME>

static inline Handle HandleCast(const ValuePtr& pa)
    { return Handle(std::dynamic_pointer_cast<Atom>(pa)); }

static inline ValuePtr ValueCast(const Handle& h)
    { return std::dynamic_pointer_cast<Value>(h); }

// Old backwards-compat function. Should be removed someday.
const Handle& truth_key(void);

// Debugging helpers see
// http://wiki.opencog.org/w/Development_standards#Print_OpenCog_Objects
// The reason indent is not an optional argument with default is
// because gdb doesn't support that, see
// http://stackoverflow.com/questions/16734783 for more explanation.
// std::string oc_to_string(const IncomingSet& iset,
//                          const std::string& indent=empty_string);
std::string oc_to_string(const Atom& atom,
                         const std::string& indent=empty_string);

/** @}*/
} // namespace opencog

namespace std {

/// Overload std::less to perform a content-based compare of the
/// AtomPtr's. Otherwise, it seems to just use the address returned
/// by `AtomPtr::get()`. The core problem is that sometimes, were were
/// expecting that `std::less<Handle>` was going to be used, and it
/// wasn't, resulting in an address-space compare. This should halt
/// that misbehavior. See issue #2371 for details.
template<>
struct less<opencog::AtomPtr>
{
    bool operator()(const opencog::AtomPtr& ata, const opencog::AtomPtr& atb) const
    {
        return ata->operator<(*atb);
    }
};

// Overloading operator<< for Incoming Set
ostream& operator<<(ostream&, const opencog::IncomingSet&);

}

#endif // _OPENCOG_ATOM_H