OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-atomspace / opencog / atomspace / AtomSpace.cc

680 lines · cc ref:
/*
 * opencog/atomspace/AtomSpace.cc
 *
 * Copyright (c) 2008-2010 OpenCog Foundation
 * Copyright (c) 2009, 2013, 2021 Linas Vepstas
 * All Rights Reserved
 *
 * 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.
 */

#include <string>
#include <iostream>
#include <fstream>
#include <list>

#include <stdlib.h>

#include <opencog/atoms/atom_types/types.h>
#include <opencog/atoms/atom_types/NameServer.h>
#include <opencog/atoms/base/Link.h>
#include <opencog/atoms/base/Node.h>
#include <opencog/atoms/parallel/TriggerLink.h>
#include <opencog/atoms/value/LinkValue.h>
#include <opencog/atoms/value/ValueFactory.h>

#include "AtomSpace.h"

using namespace opencog;

// ====================================================================

void AtomSpace::init(void)
{
    // Timestamp provides the primary ID, which the user might
    // over-write with a given name. Normally, the timestamp is
    // (almost) enough to provide a distinct, unique name...
    // expect in cases of extreme multi-threading, when multiple
    // thread can end up with the same timestamp, even at the
    // nanosecond level, dependong on the OS, scheduling, interrupts,
    // etc. So we further disambiguate (DAB) with a per-session counter.
    static std::atomic_uint64_t dabcnt(1);

    struct timespec ts;
    clock_gettime(CLOCK_REALTIME, &ts);
    char buf[64];
    strftime(buf, sizeof(buf), "/%Y-%m-%d %H:%M:%S", gmtime(&ts.tv_sec));
    sprintf(buf + strlen(buf), ".%09ld/%ld", ts.tv_nsec, dabcnt.fetch_add(1));

    _name = buf;

    // Connect signal to find out about type additions
    addedTypeConnection =
        _nameserver.typeAddedSignal().connect(
            &AtomSpace::typeAdded, this);
}

/**
 * Transient atomspaces are intended for use as scratch spaces, to hold
 * temporary results during evaluation, pattern matching and inference.
 */
AtomSpace::AtomSpace(AtomSpace* parent, bool transient) :
    Frame(ATOM_SPACE),
    _read_only(false),
    _copy_on_write(transient),
    _nameserver(nameserver()),
    addedTypeConnection(0)
{
    if (parent) {
        // Set the COW flag by default, for any Atomspace that sits on
        // top of another one. This provides a "common-sense" behavior
        // that most users would expect.
        _copy_on_write = true;
        _environ.push_back(AtomSpaceCast(parent));
        _outgoing.push_back(HandleCast(parent));
    }
    init();
}

AtomSpace::AtomSpace(const AtomSpacePtr& parent) :
    Frame(ATOM_SPACE),
    _read_only(false),
    _copy_on_write(false),
    _nameserver(nameserver()),
    addedTypeConnection(0)
{
    if (nullptr != parent) {
        // Set the COW flag by default; it seems like a simpler
        // default than setting it to be write-through.
        _copy_on_write = true;
        _environ.push_back(parent);
        _outgoing.push_back(HandleCast(parent));
    }

    init();
}

AtomSpace::AtomSpace(const HandleSeq& bases) :
    Frame(ATOM_SPACE, bases),
    _read_only(false),
    _copy_on_write(false),
    _nameserver(nameserver()),
    addedTypeConnection(0)
{
    if (0 < bases.size()) _copy_on_write = true;
    init();
}

AtomSpace::~AtomSpace()
{
    _nameserver.typeAddedSignal().disconnect(addedTypeConnection);
    clear_all_atoms();
}

/// Set up a vector of pointers to the parent AtomSpaces.
void AtomSpace::install(void)
{
    if (0 < _environ.size())
    {
        Frame::install();
        return;
    }

    // _environ will be empty, if we came through the
    // AtomSpace::AtomSpace(const HandleSeq&) ctor above.
    // We have to delay setup of _environ until after the
    // parent _atom_space is set, si that the exec below
    // works.
    for (const Handle& base : _outgoing)
    {
        // Easy. Just copy cast.
        if (_nameserver.isA(base->get_type(), ATOM_SPACE))
        {
            _environ.push_back(AtomSpaceCast(base));
            continue;
        }

        // The provided Atom in the outgoing set might be
        // executable and so we need to execute it, and
        // hopefully get an AtomSpace back. The only tricky
        // part here is that we might have been called from
        // Sexpr::decode_atom(), and so `base` might not be
        // in any AtomSpace, yet(!) So we put it in this one.
        // Duhh. Just like everything else in the world.
        if (base->is_executable())
        {
            Handle bh(base);
            if (nullptr == base->getAtomSpace())
                bh = _atom_space->add_atom(base);

            ValuePtr vp(bh->execute(this));
            AtomSpacePtr as(AtomSpaceCast(vp));
            if (nullptr == as)
                throw RuntimeException(TRACE_INFO,
                    "AtomSpace - executable base not an AtomSpace! Got %s",
                     vp->to_string().c_str());
            _environ.push_back(as);
            continue;
        }

        throw RuntimeException(TRACE_INFO,
            "AtomSpace - bases must be AtomSpaces! Got %s",
             base->to_string().c_str());
    }

    // It is the _environ and not the _outgoing that has to get it's
    // incoming set set up.
    Handle llc(get_handle());
    for (const AtomSpacePtr& has : _environ)
        has->insert_atom(llc);

    // Avoid a minor(?) amount of confusion by blanking the _atom_space
    // pointer. A non-null pointer was required, so that the above exec
    // could happen. But now that it has happened, we do not want to track
    // this any longer. Our parents are being held in the _environ vector
    // and not by this pointer.So kill it before someone else notices
    // that it is not null, and decides to use it for some nefarious
    // underhanded covert operation. Someone like Abbe Dala Pikola...
    _atom_space = nullptr;
}

void AtomSpace::remove(void)
{
    Handle lll(get_handle());
    for (const AtomSpacePtr& has : _environ)
        has->remove_atom(lll);
}

// ====================================================================

// An extremely primitive permissions system.
void AtomSpace::set_read_only(void)
{
    _read_only = true;
}

void AtomSpace::set_read_write(void)
{
    _read_only = false;
}

bool AtomSpace::content_compare(const AtomSpace& space_first,
                                const AtomSpace& space_second,
                                bool check_values,
                                bool emit_diagnostics)
{
    // Compare sizes
    if (space_first.get_num_atoms_of_type(ATOM, true) !=
        space_second.get_num_atoms_of_type(ATOM, true))
    {
        if (emit_diagnostics)
            std::cout << "compare_atomspaces - size " <<
                    space_first.get_num_atoms_of_type(ATOM, true) <<
                    " != size " <<
                    space_second.get_num_atoms_of_type(ATOM, true) <<
                    std::endl;
        return false;
    }

    // Compare each individual atom.
    // Get the atoms in each atomspace.
    HandleSeq atomsInFirstSpace, atomsInSecondSpace;
    space_first.get_handles_by_type(atomsInFirstSpace, ATOM, true);
    space_second.get_handles_by_type(atomsInSecondSpace, ATOM, true);

    // Uncheck each atom in the second atomspace.
    for (auto atom : atomsInSecondSpace)
        atom->setUnchecked();

    // Loop to see if each atom in the first has a match in the second.
    for (auto atom_first : atomsInFirstSpace)
    {
        Handle atom_second = space_second.get_atom(atom_first);

        if( false)
        {
        Handle atom_second;
        if (atom_first->is_node())
        {
            atom_second = space_second.get_node(atom_first->get_type(),
                        std::string(atom_first->get_name()));
        }
        else if (atom_first->is_link())
        {
            atom_second =  space_second.get_link(atom_first->get_type(),
                        HandleSeq(atom_first->getOutgoingSet()));
        }
        else
        {
             throw opencog::RuntimeException(TRACE_INFO,
                 "AtomSpace::compare_atomspaces - atom not Node or Link");
        }
        }

        // If the atoms don't match because one of them is null.
        if ((atom_first and not atom_second) or
            (atom_second and not atom_first))
        {
            if (emit_diagnostics)
            {
                if (atom_first)
                    std::cout << "compare_atomspaces - first atom " <<
                            atom_first->to_string() << " != NULL " <<
                            std::endl;
                if (atom_second)
                    std::cout << "compare_atomspaces - first atom " <<
                            "NULL != second atom " <<
                            atom_second->to_string() << std::endl;
            }
            return false;
        }

        // If the atoms don't match... Compare the atoms not the pointers
        // which is the default if we just use Handle operator ==.
        if (*atom_first != *atom_second)
        {
            if (emit_diagnostics)
                std::cout << "compare_atomspaces - first atom " <<
                        atom_first->to_string() << " != second atom " <<
                        atom_second->to_string() << std::endl;
            return false;
        }

        // Check the values...
        // TODO: this should probably be moved to a method on class Atom.
        if (check_values)
        {
            HandleSet keys_first = atom_first->getKeys();
            HandleSet keys_second = atom_second->getKeys();
            if (keys_first.size() != keys_second.size())
            {
                if (emit_diagnostics)
                    std::cout << "compare_atomspaces - first keys size "
                              << keys_first.size() << " != second keys size "
                              << keys_second.size() << " for "
                              << atom_first->to_short_string() << std::endl;
                return false;
            }

            if (keys_first != keys_second)
            {
                if (emit_diagnostics)
                    std::cout << "compare_atomspaces - key set mismatch for "
                              << atom_first->to_short_string() << std::endl;
                return false;
            }

            for (const Handle& key: keys_first)
            {
                ValuePtr value_first = atom_first->getValue(key);
                ValuePtr value_second = atom_second->getValue(key);
                if (*value_first != *value_second)
                {
                    if (emit_diagnostics)
                        std::cout << "compare_atomspaces - first value "
                            << value_first->to_string() << " != second value "
                            << value_second->to_string() << " for "
                            << atom_first->to_short_string() << std::endl;
                    return false;
                }
            }
        }

        // Set the check for the second atom.
        atom_second->setChecked();
    }

    // Make sure each atom in the second atomspace has been checked.
    bool all_checked = true;
    for (auto atom : atomsInSecondSpace)
    {
        if (!atom->isChecked())
        {
            if (emit_diagnostics)
                std::cout << "compare_atomspaces - unchecked space atom " <<
                        atom->to_string() << std::endl;
            all_checked = false;
        }
    }
    if (!all_checked)
        return false;

    // If we get this far, then the spaces are equal.
    return true;
}

// Name-compare only. No content-compare.
bool AtomSpace::operator==(const Atom& other) const
{
    // If other points to this, then have equality.
    if (this == &other) return true;

    if (ATOM_SPACE != other.get_type()) return false;

    // Compare the AtomSpace names, only. This will make them behave
    // like Nodes, when being inserted or removed. We can't do a
    // content compare, because it's too slow for insertion/removal.
    return get_name() == other.get_name();
}

// Imlementation is the same as Node::operator<()
bool AtomSpace::operator<(const Atom& other) const
{
    // If other points to this, then have equality.
    if (this == &other) return false;

    ContentHash cht = get_hash();
    ContentHash cho = other.get_hash();
    if (cht != cho) return cht < cho;

    // We get to here only if the hashes are equal. This is
    // extremely unlikely; it requires a 64-bit hash collision.
    // Compare the contents directly, for this case.
    if (get_type() != other.get_type())
        return get_type() < other.get_type();

    return get_name() < other.get_name();
}

ContentHash AtomSpace::compute_hash() const
{
	ContentHash hsh = std::hash<std::string>()(get_name());

	// Nodes will never have the MSB set.
	ContentHash mask = ~(((ContentHash) 1ULL) << (8*sizeof(ContentHash) - 1));
	hsh &= mask;
	return hsh;
}

// ====================================================================
// Provide all of the virtual methods on the base class.

const std::string& AtomSpace::get_name() const
{
	return _name;
}

// In order to restore complex AtomSpace DAG's from storage, we need
// to be able to set their names, to match what is in storage. So the
// name is settable.
void AtomSpace::set_name(const std::string& newna)
{
	_name = newna;
	_content_hash = compute_hash();
}

Handle AtomSpace::getOutgoingAtom(Arity n) const
{
	if (_outgoing.size() <= n) return Handle::UNDEFINED;
	return _outgoing[n];
}

// ====================================================================
/// Search for the given AtomSpace in our stack. Return a count to
/// the shallowest copy found. Return -1 if the AtomSpace is not found
/// in the stack.
// Tail-recursive implementation, should be fast.
int AtomSpace::depth(const AtomSpace* as) const
{
    if (nullptr == as) return -1;
    if (as == this) return 0;

    // As long as _environ is exactly one in size, just loop.
    int lvl = 1;
    const std::vector<AtomSpacePtr>* env = &_environ;
    while (true)
    {
        size_t evs = env->size();
        if (0 == evs) return -1;
        if (1 < evs) break;
        if (as == (*env)[0].get()) return lvl;
        lvl++;
        env = &((*env)[0]->_environ);
    }

    // Hunt for shallowest.
    int shallowest = -1;
    for (const AtomSpacePtr& base : _environ)
    {
        int d = base->depth(as);
        if (0 <= d and
            ((-1 == shallowest) or (d < shallowest)))
        {
            shallowest = d;
        }
    }
    if (0 <= shallowest)
        return lvl + shallowest;

    return -1;
}

int AtomSpace::depth(const Handle& atom) const
{
    if (nullptr == atom) return -1;
    AtomSpace* as = atom->getAtomSpace();
    if (as == this) return 0;
    return depth(as);
}

bool AtomSpace::in_environ(const AtomSpace* as) const
{
    if (nullptr == as) return false;
    if (as == this) return true;
    const std::vector<AtomSpacePtr>* env = &_environ;
    while (true)
    {
        size_t evs = env->size();
        if (0 == evs) return false;
        if (1 < evs) break;
        if (as == (*env)[0].get()) return true;
        env = &((*env)[0]->_environ);
    }
    for (const AtomSpacePtr& base : *env)
    {
        if (base->in_environ(as)) return true;
    }
    return false;
}

bool AtomSpace::in_environ(const Handle& atom) const
{
    if (nullptr == atom) return false;
    AtomSpace* as = atom->getAtomSpace();
    if (as == this) return true;
    for (const AtomSpacePtr& base : _environ)
    {
        if (base->in_environ(as)) return true;
    }
    return false;
}

// ====================================================================

Handle AtomSpace::add_atom(const Handle& h)
{
    // Cannot add atoms to a read-only atomspace. But if it's already
    // in the atomspace, return it.
    if (_read_only) return get_atom(h);

    // If it is a DeleteLink, then the addition will fail. Deal with it.
    // If its a GrantLink, addition might require extra care.
    try {
        return add(h);
    }
    catch (const DeleteException& ex) { /* Do nothing */ }
    catch (const ValueReturnException& ex) {
        // Rethrow to pass results onwards (to scheme/python bindings)
        throw;
    }
    catch (const SilentException& ex) {
        // The SilentException is thrown by GrantLink, when the
        // user attempts grants in non-base Frames. We want to
        // disallow hiding of grants, so we end up here.
        return lookupHide(h, false);  // Do not allow hiding!
    }
    return Handle::UNDEFINED;
}

ValuePtr AtomSpace::add_atoms(const ValuePtr& vptr)
{
    if (nullptr == vptr) return vptr;

    Type t = vptr->get_type();
    if (nameserver().isA(t, ATOM))
    {
        Handle h = add_atom(HandleCast(vptr));
        if (h) return h; // Might be null if AtomSpace is read-only.
        return vptr;
    }

    if (nameserver().isA(t, LINK_VALUE))
    {
        std::vector<ValuePtr> vvec;
        for (const ValuePtr& v : LinkValueCast(vptr)->value())
           vvec.push_back(add_atoms(v));

        return valueserver().create(t, vvec);
    }
    return vptr;
}

// COW == Copy On Write
#define COWBOY_CODE(DO_STUFF)                                            \
    AtomSpace* has = h->getAtomSpace();                                  \
                                                                         \
    /* Hmm. It's kind-of a user-error, if they give us a naked atom.  */ \
    /* We could throw here, and force them to fix their code, or we   */ \
    /* can silently do what they wanted!? Which will probably expose  */ \
    /* other hard-to-debug bugs in the user's code ...                */ \
    /* if (nullptr == has)                                            */ \
    /*     throw opencog::RuntimeException(TRACE_INFO,                */ \
    /*            "Your atom is needs to be placed in an atomspace!") */ \
                                                                         \
    if (_read_only) {                                                    \
        throw opencog::RuntimeException(TRACE_INFO,                      \
             "Value not changed; AtomSpace is readonly");                \
        return Handle::UNDEFINED;                                        \
    }                                                                    \
                                                                         \
    /* No copy needed. Safe to just update.                           */ \
    if (has == this) {                                                   \
        DO_STUFF(h);                                                     \
        return h;                                                        \
    }                                                                    \
                                                                         \
    /* If the atom is in a read-only atomspace (i.e. if the parent    */ \
    /* is read-only) and this atomspace is read-write, then make      */ \
    /* a copy of the atom, and then set the value.                    */ \
    /* If this is a COW space, then always copy, no matter what.      */ \
    if (nullptr == has or has->_read_only or _copy_on_write or           \
        not in_environ(has))                                             \
    {                                                                    \
        /* Copy the atom into this atomspace                          */ \
        Handle copy(add(h, true));                                       \
        DO_STUFF(copy);                                                  \
        return copy;                                                     \
    }                                                                    \
                                                                         \
    DO_STUFF(h);                                                         \
    return h;


// Copy-on-write for setting values.
Handle AtomSpace::set_value(const Handle& h,
                            const Handle& key,
                            const ValuePtr& value)
{
	// Skip R/O and COW checking if key is a message.
	if (h->usesMessage(key))
	{
		h->setValue(key, value);
		return h;
	}

	#define SETV(atm) atm->setValue(key, value);
	COWBOY_CODE(SETV);
}

// The increment is atomic i.e. thread-safe.
Handle AtomSpace::increment_count(const Handle& h, const Handle& key,
                                  const std::vector<double>& count)
{
	#define INCR_CNT(atm) atm->incrementCount(key, count);
	COWBOY_CODE(INCR_CNT);
}

// The increment is atomic i.e. thread-safe.
Handle AtomSpace::increment_count(const Handle& h, const Handle& key,
                                  size_t ref, double count)
{
	#define INCR_LOC(atm) atm->incrementCount(key, ref, count);
	COWBOY_CODE(INCR_LOC);
}

std::string AtomSpace::to_string(void) const
{
	std::stringstream ss;
	ss << *this;
	return ss.str();
}

/// Pretty-print with proper indentation.
/// The idea is that it should be easier to understand the hierarchy.
/// For example:
///    (use-modules (opencog))
///    (define space1 (cog-atomspace))
///    (define space2 (cog-new-atomspace space1))
///    (define space3 (cog-new-atomspace space2))
///    (define space4 (cog-new-atomspace space3))
///    (define space5a (cog-new-atomspace space4))
///    (define space5b (cog-new-atomspace space4))
///    (define space5c (cog-new-atomspace space4))
///    (define space6 (cog-new-atomspace space5a space5b space5c))
///    space6
///
std::string AtomSpace::to_string(const std::string& indent) const
{
	std::string sexpr = indent + "(AtomSpace \"" + _name + "\"";
	size_t szo = _outgoing.size();
	if (0 == szo)
		return sexpr + ")\n";

	if (1 == szo)
		return sexpr + "\n   " + _outgoing[0]->to_string(indent) + ")";

	sexpr += "\n";
	std::string idmore = indent + "   ";
	for (const Handle& ho : _outgoing)
		sexpr += ho->to_string(idmore) + "\n";
	sexpr += ")\n";

	return sexpr;
}

std::string AtomSpace::to_short_string(const std::string& indent) const
{
	return indent + "(AtomSpace \"" + _name + "\")";
}

namespace std {

ostream& operator<<(ostream& out, const opencog::AtomSpace& as)
{
    HandleSeq hseq;
    as.get_root_set_by_type(hseq, opencog::ATOM, true);
    for (const opencog::Handle& h : hseq)
		  out << h->to_string() << std::endl;
    return out;
}

} // namespace std