/*
* opencog/atoms/execution/EvaluatableLink.cc
*
* Copyright (C) 2009, 2013, 2014, 2015 Linas Vepstas
* Copyright (C) 2025 BrainyBlaze Dynamics, LLC
*
* 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.
*
* You should have received a copy of the GNU Affero General Public
* License along with this program; if not, write to:
* Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include <opencog/atoms/atom_types/atom_types.h>
#include <opencog/atoms/core/NumberNode.h>
#include <opencog/atoms/free/FindUtils.h>
#include <opencog/atoms/grant/DefineLink.h>
#include <opencog/atoms/pattern/PatternLink.h>
#include <opencog/atoms/scope/LambdaLink.h>
#include <opencog/atoms/value/LinkValue.h>
#include <opencog/atomspace/AtomSpace.h>
#include "EvaluatableLink.h"
#include <algorithm>
#include <cmath>
using namespace opencog;
EvaluatableLink::EvaluatableLink(const HandleSeq&& oset, Type t)
: Link(std::move(oset), t), _unordered(false)
{
if (not nameserver().isA(t, EVALUATABLE_LINK))
{
const std::string& tname = nameserver().getTypeName(t);
throw InvalidParamException(TRACE_INFO,
"Expecting an EvaluatableLink, got %s", tname.c_str());
}
// A kind of a hack, to do what the UnorderedLink ctor does.
// We do this mostly because we're lazy about writing lots
// lots of c++ classes to handle each of the case statemens
// below. Even though doing that would be "technically more
// correct". But for now, this technical hack seems ... tolerable.
// I really don't want to be pedantic for this stuff. It seems
// OK to wing it, sometimes...
if (nameserver().isA(t, UNORDERED_SIG))
{
_unordered = true;
// Place into arbitrary, but deterministic order. We use
// content (hash) based less, to avoid variations due to
// address-space randomization.
std::sort(_outgoing.begin(), _outgoing.end(),
content_based_handle_less());
}
}
// ---------------------------------------------------------------
/// Extract a single floating-point double out of an atom, that,
/// when executed, should yield a value containing a number.
/// Viz, either a NumberNode, or a FloatValue.
static double get_numeric_value(AtomSpace* as, bool silent, const Handle& h)
{
ValuePtr pap(h->execute(as, silent));
Type t = pap->get_type();
if (nameserver().isA(t, LINK_VALUE))
{
pap = LinkValueCast(pap)->value()[0];
t = pap->get_type();
}
if (NUMBER_NODE == t)
{
NumberNodePtr n(NumberNodeCast(pap));
return n->get_value();
}
if (nameserver().isA(t, FLOAT_VALUE))
{
FloatValuePtr fv(FloatValueCast(pap));
if (fv->value().empty())
throw RuntimeException(TRACE_INFO, "FloatValue is empty!");
return fv->value()[0];
}
// Typical case is a VoidValue for `pap`.
if (silent) throw SilentException();
throw SyntaxException(TRACE_INFO,
"Don't know how to do arithmetic with this: %s",
pap->to_string().c_str());
return std::nan("");
}
/// Perform a GreaterThan check
static bool greater(AtomSpace* as, const Handle& h, bool silent)
{
const HandleSeq& oset = h->getOutgoingSet();
if (2 != oset.size())
throw SyntaxException(TRACE_INFO,
"GreaterThankLink expects two arguments");
double v0 = get_numeric_value(as, silent, oset[0]);
double v1 = get_numeric_value(as, silent, oset[1]);
return (v0 > v1);
}
static bool lesser(AtomSpace* as, const Handle& h, bool silent)
{
const HandleSeq& oset = h->getOutgoingSet();
if (2 != oset.size())
throw SyntaxException(TRACE_INFO,
"GreaterThankLink expects two arguments");
double v0 = get_numeric_value(as, silent, oset[0]);
double v1 = get_numeric_value(as, silent, oset[1]);
return (v0 < v1);
}
static ValuePtr exec_or_eval(AtomSpace* as,
const Handle& term,
AtomSpace* scratch,
bool silent)
{
// PutLinks lie to us. That's OK, for now...
// if (term->is_evaluatable())
if (term->is_type(EVALUATABLE_LINK))
{
bool crisp = term->bevaluate(scratch, silent);
return ValueCast(createBoolValue(crisp));
}
// Nothing to do, here.
if (VALUE_SHIM_LINK == term->get_type())
return term->execute(scratch);
// Argh. The ValueShimLink might be buried deeper in the expression.
// In this case, ValueShimLink::setAtomSpace() will throw a
// RuntimeException. Catch that. Perhaps it should be changed to
// a SilentException? Except that SilentExceptions are hard to
// debug, because they don't set a message,
Handle sterm;
try
{
sterm = scratch->add_atom(term);
}
catch (const RuntimeException& ex)
{
return term->execute(scratch);
}
if (not sterm->is_executable())
return sterm;
ValuePtr vp(sterm->execute(scratch, silent));
// If the return value is a ContainerValue, we assume that this
// is the result of executing a MeetLink or QueryLink.
// In this case, unwrap it, to get the "actual value".
// This feels slightly hacky, but will do for just right now.
if (vp->is_type(CONTAINER_VALUE))
{
HandleSeq hs(LinkValueCast(vp)->to_handle_seq());
if (1 == hs.size())
vp = hs[0];
}
// XXX FIXME? Should we be doing the add here? Isn't this the
// callers reponsibility? Three unit tests fail, if we don't
// do the add: RandomUTest FormulaUTest SequenceUTest and one
// fails: SequenceUTest if we add to scratch instead of main.
// I'm not sure if something is buggy or not, or if this is an
// intentional design choice. FWIW there are many places in the
// code base that do something like this.
// if (vp->is_atom()) scratch->add_atom(HandleCast(vp));
if (vp->is_atom()) as->add_atom(HandleCast(vp));
return vp;
}
/// Return true, if any Atom in the outgoing set is being used as
/// a key somewhere.
static bool is_key(const Handle& h)
{
for (const Handle& ho : h->getOutgoingSet())
if (ho->isKey()) return true;
return false;
}
/// Return true, if any Atom in the outgoing set is being used as
/// a message somewhere.
static bool is_message(const Handle& h)
{
for (const Handle& ho : h->getOutgoingSet())
if (ho->isMessage()) return true;
return false;
}
/// Check for set membership
static bool member(AtomSpace* as, const Handle& h, bool silent)
{
const HandleSeq& oset = h->getOutgoingSet();
if (2 != oset.size())
throw SyntaxException(TRACE_INFO,
"MemberLink expects two arguments");
ValuePtr v0(exec_or_eval(as, oset[0], as, silent));
ValuePtr v1(exec_or_eval(as, oset[1], as, silent));
// Is v0 a member of v1? This question makes sense only
// if v0 is an atom and v1 is a set.
if (not v0->is_atom()) return false;
if (not nameserver().isA(v1->get_type(), SET_LINK)) return false;
for (const Handle& hs: HandleCast(v1)->getOutgoingSet())
{
if (v0 == hs) return true;
}
return false;
}
/// Check for subset relationship
static bool subset(AtomSpace* as, const Handle& h, bool silent)
{
const HandleSeq& oset = h->getOutgoingSet();
if (2 != oset.size())
throw SyntaxException(TRACE_INFO,
"SubsetLink expects two arguments");
ValuePtr v0(exec_or_eval(as, oset[0], as, silent));
ValuePtr v1(exec_or_eval(as, oset[1], as, silent));
// Is v0 a subset of v1? This question makes sense only
// if v0 and v1 are sets.
if (not nameserver().isA(v0->get_type(), SET_LINK)) return false;
if (not nameserver().isA(v1->get_type(), SET_LINK)) return false;
const HandleSeq& superset(HandleCast(v1)->getOutgoingSet());
for (const Handle& h0: HandleCast(v0)->getOutgoingSet())
{
bool found = false;
for (const Handle& h1 : superset)
{
if (h0 == h1) {found = true; break;}
}
if (not found) return false;
}
return true;
}
/** Return true if the SatisfactionLink can be "trivially" evaluated. */
static bool is_evaluatable_sat(const Handle& satl)
{
if (1 != satl->get_arity())
return false;
PatternLinkPtr plp(PatternLinkCast(satl));
return 0 == plp->get_variables().varseq.size();
}
/** Return true, if `thish` is tail-recursive */
static bool is_tail_rec(const Handle& thish, const Handle& tail)
{
if (DEFINED_PREDICATE_NODE != tail->get_type())
return false;
Handle defn(DefineLink::get_definition(tail));
if (defn == thish)
return true;
if (SATISFACTION_LINK != defn->get_type())
return false;
if (not is_evaluatable_sat(defn))
return false;
if (thish == defn->getOutgoingAtom(0))
return true;
return false;
}
static bool do_tail_eval(AtomSpace* scratch, bool silent, const Handle& h)
{
if (DEFINED_PREDICATE_NODE == h->get_type())
{
Handle defn(DefineLink::get_definition(h));
if (not defn->is_evaluatable())
throw RuntimeException(TRACE_INFO,
"Expecting EvaluatableLink, got %s\n",
defn->to_string().c_str());
return defn->bevaluate(scratch, silent);
}
if (not h->is_evaluatable())
throw RuntimeException(TRACE_INFO,
"Expecting EvaluatableLink, got %s\n",
h->to_string().c_str());
return h->bevaluate(scratch, silent);
}
// ---------------------------------------------------------------
/// EvaluatableLink::bevaluate() -- evaluate this link, returning
/// a crisp-logic true/false value.
///
/// This handles evaluation of crisp Boolean-logic constructs including:
/// - Logical constants (TrueLink, FalseLink)
/// - Logical connectives (NotLink, AndLink, OrLink, SequentialAndLink, etc.)
/// - Comparison operations (GreaterThanLink, EqualLink, IdenticalLink, etc.)
///
bool EvaluatableLink::bevaluate(AtomSpace* scratch, bool silent)
{
Type t = get_type();
Handle evelnk(get_handle());
// Logical constants
if (TRUE_LINK == t or FALSE_LINK == t)
{
// Assume that the link is wrapping something executable (or
// evaluatable), which we execute (or evaluate), but then
// ignore the result. The executable ones, we need to put the
// result in the (scratch) atomspace ... but in either case,
// we ignore the TV on it. We are doing this for the side-effects,
// of course -- the True/FalseLinks are pure side-effect atoms.
//
// We instantiate/evaluate in the main atomspace, however.
// This is subtle, so listen-up: one of the side effects
// might involve evaluating some condition, which then pokes
// atoms into the atomspace, to signal some event or state.
// These cannot be discarded. This is explicitly tested by
// SequenceUTest::test_or_put().
for (const Handle& term : _outgoing)
exec_or_eval(_atom_space, term, scratch, silent);
if (TRUE_LINK == t) return true;
return false;
}
// -------------------------
// Crisp-binary-valued Boolean Logical connectives
if (NOT_LINK == t)
{
if (not _outgoing[0]->is_evaluatable())
throw RuntimeException(TRACE_INFO,
"Expecting EvaluatableLink, got %s\n",
_outgoing[0]->to_string().c_str());
return not _outgoing[0]->bevaluate(scratch, silent);
}
if (AND_LINK == t)
{
for (const Handle& h : _outgoing)
{
if (not h->is_evaluatable())
throw RuntimeException(TRACE_INFO,
"Expecting EvaluatableLink, got %s\n",
h->to_string().c_str());
bool tv = h->bevaluate(scratch, silent);
if (not tv) return false;
}
return true;
}
if (OR_LINK == t)
{
for (const Handle& h : _outgoing)
{
if (not h->is_evaluatable())
throw RuntimeException(TRACE_INFO,
"Expecting EvaluatableLink, got %s\n",
h->to_string().c_str());
bool tv = h->bevaluate(scratch, silent);
if (tv) return true;
}
return false;
}
if (SEQUENTIAL_AND_LINK == t)
{
size_t arity = _outgoing.size();
if (0 == arity) return true;
// Is this tail-recursive? If so, then handle it.
bool is_trec = is_tail_rec(evelnk, _outgoing[arity-1]);
if (is_trec) arity--;
// Loop at least once. If tail-recursive, loop forever.
do
{
for (size_t i=0; i<arity; i++)
{
bool tv = do_tail_eval(scratch, silent, _outgoing[i]);
if (not tv) return false;
}
} while (is_trec);
return true;
}
if (SEQUENTIAL_OR_LINK == t)
{
size_t arity = _outgoing.size();
if (0 == arity) return false;
// Is this tail-recursive? If so, then handle it.
bool is_trec = is_tail_rec(evelnk, _outgoing[arity-1]);
if (is_trec) arity--;
// Loop at least once. If tail-recurive, loop forever.
do
{
for (size_t i=0; i<arity; i++)
{
bool tv = do_tail_eval(scratch, silent, _outgoing[i]);
if (tv) return true;
}
} while (is_trec);
return false;
}
// -------------------------
// Assorted relations
if (GREATER_THAN_LINK == t) return greater(scratch, evelnk, silent);
if (LESS_THAN_LINK == t) return lesser(scratch, evelnk, silent);
if (MEMBER_LINK == t) return member(scratch, evelnk, silent);
if (SUBSET_LINK == t) return subset(scratch, evelnk, silent);
if (IS_KEY_LINK == t) return is_key(evelnk);
if (IS_MESSAGE_LINK == t) return is_message(evelnk);
throw RuntimeException(TRACE_INFO,
"Implementation Error: Cannot evaluate %s", to_string().c_str());
}
// ---------------------------------------------------------------
DEFINE_LINK_FACTORY(EvaluatableLink, EVALUATABLE_LINK)
/* ===================== END OF FILE ===================== */