OverviewFilesCommitsIssuesDocsReleasesPeers

opencog-atomspace / opencog / atoms / scope / PutLink.cc

639 lines · cc ref:
/*
 * opencog/atoms/scope/PutLink.cc
 *
 * Copyright (C) 2015 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.
 *
 * 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/base/ClassServer.h>
#include <opencog/atoms/value/LinkValue.h>
#include <opencog/atomspace/AtomSpace.h>
#include <opencog/atoms/grant/DefineLink.h>
#include "LambdaLink.h"
#include "PutLink.h"

using namespace opencog;

PutLink::PutLink(const HandleSeq&& oset, Type t)
    : PrenexLink(std::move(oset), t)
{
	init();
}

/* ================================================================= */

/// PutLink expects a very strict format: an arity-2 link, with
/// the first part being a pattern, and the second a list or set
/// of arguments. If the pattern has N variables, then the second
/// part must have N arguments.  Furthermore, any type restrictions
/// on the variables must be satisfied by the arguments.
///
/// The following formats are understood:
///
///    PutLink
///       <pattern with 1 variable>
///       <any single atom>
///
///    PutLink
///       <pattern with N variables>
///       ListLink     ;; must have arity N
///          <atom 1>
///          ...
///          <atom N>
///
/// The below is a handy-dandy easy-to-use form. When it is reduced,
/// it will result in the creation of a set of reduced forms, not
/// just one (the two sets having the same arity). Unfortunately,
/// this trick cannot work for N=1 unless the variable is cosntrained
/// to not be a set.
///
///    PutLink
///       <pattern with N variables>
///       SetLink        ;; Must hold a set of ListLinks
///          ListLink    ;; must have arity N
///             <atom 1>
///             ...
///             <atom N>
///
void PutLink::init(void)
{
	if (not nameserver().isA(get_type(), PUT_LINK))
		throw InvalidParamException(TRACE_INFO, "Expecting a PutLink");

	size_t sz = _outgoing.size();
	if (2 != sz and 3 != sz)
		throw InvalidParamException(TRACE_INFO,
			"Expecting an outgoing set size of two or three, got %d; %s",
			sz, to_string().c_str());

	ScopeLink::extract_variables(true);

	if (2 == sz)
	{
		// If the body is just a single variable, and there are no
		// type declarations for it, then ScopeLink gets confused.
		_vardecl = Handle::UNDEFINED;
		_body = _outgoing[0];
		_arguments = _outgoing[1];
	}
	else
	{
		// ScopeLink::extract_variables does not assign _vardecl and
		// _body if variable declaration is unquoted. (Re)do it here.
		_vardecl = _outgoing[0];
		_body = _outgoing[1];
		_arguments = _outgoing[2];
	}

	static_typecheck_arguments();
}

/// Check that the arguments in the PutLink obey the type constraints.
/// This only performs "static" typechecking, at construction-time;
/// since the arguments may be dynamically obtained at run-time, we cannot
/// check these here.
///
/// Why bother? I'm not sure. The primary use case for PutLink is that
/// either it is constructed on the fly, as a part of rule reduction,
/// or it has dynamically evaluatable terms. The latter case cannot be
/// be static-typechecked. The former case is incoherent: if some
/// subsystem is constructing these PutLinks on the fly, it it is
/// constructing them so badly that an exception triggers on the static
/// typecheck, what's the point? It says the rule that built this is
/// broken. But automated systems can only handle silent exceptions...
/// and not the ones below. So the ones below, who are they for? The
/// human who is writing broken rules, and needs debugging help?
///
/// This kind of stati typechecking made sense in the early days of
/// Atomese. But now it's inappropriate ... the whole idea needs to
/// be replaced by some sheaf/connector system that can assemble
/// processing pipelines correctly, instead of throwing exceptions
/// when subsystems are mis-connected.
///
void PutLink::static_typecheck_arguments(void)
{
	// Cannot typecheck at this point in time, because the schema
	// might not be defined yet...
	Type btype = _body->get_type();
	if (DEFINED_SCHEMA_NODE == btype)
		return;
	if (DEFINED_PREDICATE_NODE == btype)
		return;
	if (PUT_LINK == btype)
		return;

	// If its part of a signature, there is nothing to do.
	if (nameserver().isA(btype, TYPE_NODE) or TYPE_CHOICE == btype)
		return;

	Handle valley = _arguments;
	Type vtype = valley->get_type();

	// If it's body or argument is an UnquoteLink then the PutLink is
	// likely quoted and thus there is nothing to do
	if (btype == UNQUOTE_LINK or vtype == UNQUOTE_LINK)
		return;

	// If its a LambdaLink, try to see if its eta-reducible. If
	// so, then eta-reduce it immediately. A LambdaLink is
	// eta-reducible when it's body is a ListLink.
	if (LAMBDA_LINK == vtype)
	{
		LambdaLinkPtr lam(LambdaLinkCast(_arguments));
		Handle body = lam->get_body();

		// The body might not exist, if there's an unmantched
		// UnquoteLink in it.  I really dislike Quote/Unquote.
		// There's something deeply evil about them.
		if (nullptr == body)
			return;

		Type bt = body->get_type();
		if (LIST_LINK == bt)
		{
			valley = body;
			vtype = bt;
		}
	}

	size_t sz = _variables.varseq.size();
	if (1 == sz)
	{
		if (not _variables.is_type(valley)
		    and SET_LINK != vtype
		    and PUT_LINK != vtype
		    and not (nameserver().isA(vtype, SATISFYING_LINK)))
		{
			// Well, one more possible case ...
			// Function composition with lambda means that
			// the body of the lambda must be the right type.
			if (LAMBDA_LINK == vtype)
			{
				LambdaLinkPtr lam(LambdaLinkCast(valley));
				const Handle& body = lam->get_body();
				if (_variables.is_type(body))
					return; // everything is OK.
			}

			throw InvalidParamException(TRACE_INFO,
				"PutLink mismatched type!");
		}
		return;
	}

	// Cannot typecheck naked FunctionLinks.  For example:
	// (cog-execute! (Put (Plus) (List (Number 2) (Number 2))))
	if (0 == sz and nameserver().isA(btype, FUNCTION_LINK))
		return;

	// The standard, default case is to get a ListLink as an argument.
	if (LIST_LINK == vtype)
	{
		// is_type() verifies that the arity of the vars
		// and the arguments matches up.
		if (not _variables.is_type(valley->getOutgoingSet()))
		{
			if (_vardecl)
				throw SyntaxException(TRACE_INFO,
					"PutLink has mismatched argument list! vardecl=%s\nvals=%s",
					_vardecl->to_string().c_str(),
					_arguments->to_string().c_str());
			else
				throw SyntaxException(TRACE_INFO,
					"PutLink has mismatched argument list! body=%s\nvals=%s",
					_body->to_string().c_str(),
					_arguments->to_string().c_str());
		}
		return;
	}

	// If its part of a signature, there is nothing to do.
	if (TYPE_NODE == vtype or TYPE_CHOICE == vtype)
		return;

	// The only remaining possibility is that there is set of ListLinks.
	if (SET_LINK != vtype)
		throw InvalidParamException(TRACE_INFO,
			"PutLink was expecting a ListLink, SetLink or GetLink!");

	if (0 == sz)
		throw InvalidParamException(TRACE_INFO,
			"PutLink: cannot put the empty set!");

	if (1 < sz)
	{
		for (const Handle& h : valley->getOutgoingSet())
		{
			// If the arity is greater than one,
			// then the arguments must be in a list.
			if (h->get_type() != LIST_LINK)
				throw InvalidParamException(TRACE_INFO,
					"PutLink expected argument list!");

			if (not _variables.is_type(h->getOutgoingSet()))
				throw InvalidParamException(TRACE_INFO,
					"PutLink bad argument list!");
		}
		return;
	}

	// If the arity is one, the arguments must obey type constraint.
	for (const Handle& h : valley->getOutgoingSet())
	{
		if (not _variables.is_type(h))
			throw InvalidParamException(TRACE_INFO,
					"PutLink bad type!");
	}
}

/* ================================================================= */

static inline Handle reddy(PrenexLinkPtr& subs, const HandleSeq& oset)
{
	subs->make_silent(true);
	return subs->beta_reduce(oset);
}

// If arg is executable, then run it, and unwrap the set link, too.
// We unwrap the SetLinks because that is what GetLinks return.
static inline Handle expand(const Handle& arg, bool silent)
{
	Handle result(arg);
	if (arg->is_executable())
	{
		ValuePtr vp = arg->execute();
		if (LINK_VALUE == vp->get_type())
			result = createLink(std::move(LinkValueCast(vp)->to_handle_seq()), SET_LINK);
		else if (vp->is_atom())
			result = HandleCast(vp);
	}

	if (SET_LINK == result->get_type())
	{
		Arity n = result->get_arity();
		if (1 == n)
			result = result->getOutgoingAtom(0);

		if (0 == n)
		{
			if (silent) throw SilentException();
			throw RuntimeException(TRACE_INFO, "Cannot put the empty set!");
		}
	}
	return result;
}

/**
 * Perform the actual beta reduction --
 *
 * Substitute arguments for the variables in the pattern tree.
 * This is not a pure beta-reduction, because it does execute
 * any executable arguments, so as to obtain the correct forms
 * to place into the reduction.  This does make the PutLink
 * resemble function application; however, here, the application
 * is not infinite-recursive; it is only one level deep.  There
 * is just enough execution performed to get the need arguments,
 * and no more.
 *
 * What this actually does is fairly complex and sophisticated.
 * First, when LambdaLinks are involved, and the arguments are
 * variables, it performs alpha conversion instead of beta reduction.
 * (as that's kind-of "the same thing", for variables).
 *
 * When multiple arguments are presented as a  set, then the put
 * is applied to each member of the set; the result is a set.
 *
 * When the set is a singleton, that singleton is unwrapped, and
 * the set is discarded. This is done in order to play nice with
 * GetLink, which always returns SetLinks, sometimes uneccesarily.
 *
 * Users who need a more complete apply-like environment should
 * look to the EvaluationLink or the ExecutionOutputLink.
 *
 * So, for example, if the PutLink looks like this:
 *
 *   PutLink
 *      EvaluationLink
 *         PredicateNode "is a kind of"
 *         ListLink
 *            VariableNode $a
 *            ConceptNode "hot patootie"
 *      ConceptNode "cowpie"
 *
 * then the reduced body will be
 *
 *   EvaluationLink
 *      PredicateNode "is a kind of"
 *      ListLink
 *         ConceptNode "cowpie"
 *         ConceptNode "hot patootie"
 *
 * Type checking is performed during substitution; if the arguments fail
 * to have the desired types, no substitution is performed, and a
 * TypeCheckException is thrown (from Variables::substitute() circa
 * line 440.) This is a SilentException; it is not logged.
 *
 * For set substitutions, this acts as a filter, removing (filtering out)
 * the mismatched types. Tested in PutLinkUTest::test_filtering()
 *
 * Note that the resulting tree is NOT placed into any atomspace!
 */
Handle PutLink::do_reduce(void) const
{
	Handle bods(_body);
	Variables vars(_variables);
	PrenexLinkPtr subs(PrenexLinkCast(get_handle()));
	Handle args(_arguments);
	ValuePtr vargs(_arguments);

	// Most arguments can be executed (should be executed) before
	// reduction. This includes queries and functions; failing to
	// do so can result in unintended infinite loops. Examples
	// include MeetLinks, which are run, to determine what to plug in.
	Type t = _arguments->get_type();
	if (nameserver().isA(t, SATISFYING_LINK) or
	    nameserver().isA(t, FUNCTION_LINK))
	{
		vargs = _arguments->execute();
		if (nullptr == vargs)
			throw SyntaxException(TRACE_INFO,
			       "Execution must result in an Atom!");
		if (vargs->is_atom())
			args = HandleCast(vargs);
	}

	// Resolve the body, if needed. That is, if the body is
	// given in a definition, get that definition.
	Type btype = _body->get_type();
	if (DEFINED_SCHEMA_NODE == btype or
	    DEFINED_PREDICATE_NODE == btype)
	{
		bods = DefineLink::get_definition(bods);
		btype = bods->get_type();
		// XXX TODO we should perform a type-check on the function.
		if (not nameserver().isA(btype, LAMBDA_LINK))
			throw InvalidParamException(TRACE_INFO,
					"Expecting a LambdaLink, got %s",
			      bods->to_string().c_str());
	}

	// If the body is itself a PutLink, then reduce it first
	if (PUT_LINK == btype)
	{
		PutLinkPtr nested_put = PutLinkCast(bods);
		nested_put->make_silent(_silent);
		bods = nested_put->do_reduce();
		btype = bods->get_type();
		try {
			subs = createPutLink(HandleSeq({bods, args}));
		}
		catch (const InvalidParamException& ex) {
			throw SyntaxException(TRACE_INFO,
				"Can't execute: bad syntax: %s",
				to_string().c_str());
		}
	}

	// If the body is a lambda, work with that.
	if (nameserver().isA(btype, LAMBDA_LINK))
	{
		LambdaLinkPtr lam(LambdaLinkCast(bods));
		bods = lam->get_body();
		vars = lam->get_variables();
		btype = bods->get_type();
		subs = lam;
	}

	// Now get the arguments that we will plug into the body.
	Type vtype = vargs->get_type();
	size_t nvars = vars.varseq.size();

	// FunctionLinks behave like locale-less (pointless) lambdas;
	// that is, one can create valid beta-redexes with them.
	// We handle that here.
	//
	// At this time, we don't know the number of arguments any given
	// FunctionLink might take.  Atomese does have the mechanisms
	// to declare these, including arbitrary-arity functions, its
	// just that it is currently not declared anywhere for any of the
	// FunctionLinks.  So we just guess as best we can.  Example usage:
	// (cog-execute! (Put (Plus) (List (Number 2) (Number 2))))
	// (cog-execute! (Put (Plus (Number 9)) (List (Number 2) (Number 2))))
	if (0 == nvars and nameserver().isA(btype, FUNCTION_LINK))
	{
		if (nameserver().isA(vtype, LINK_VALUE))
		{
			HandleSeq oset(bods->getOutgoingSet());
			HandleSeq argl(LinkValueCast(vargs)->to_handle_seq());
			for (const Handle& arg : argl)
				oset.emplace_back(expand(arg, _silent));
			return createLink(std::move(oset), btype);
		}

		if (LIST_LINK == vtype)
		{
			HandleSeq oset(bods->getOutgoingSet());
			for (const Handle& arg : args->getOutgoingSet())
				oset.emplace_back(expand(arg, _silent));
			return createLink(std::move(oset), btype);
		}

		if (SET_LINK != vtype)
		{
			HandleSeq oset(bods->getOutgoingSet());
			oset.emplace_back(args);
			return createLink(std::move(oset), btype);
		}

		// If the arguments are given in a set, then iterate over the set...
		HandleSeq bset;
		for (const Handle& h : args->getOutgoingSet())
		{
			if (LIST_LINK == h->get_type())
			{
				HandleSeq oset(bods->getOutgoingSet());
				for (const Handle& arg : h->getOutgoingSet())
					oset.emplace_back(expand(arg, _silent));
				bset.emplace_back(createLink(std::move(oset), btype));
			}
			else
			{
				HandleSeq oset(bods->getOutgoingSet());
				oset.emplace_back(expand(h, _silent));
				bset.emplace_back(createLink(std::move(oset), btype));
			}
		}
		return createLink(std::move(bset), SET_LINK);
	}

	// If there is only one variable in the PutLink body...
	if (1 == nvars)
	{
		if (nameserver().isA(vtype, LINK_VALUE))
		{
			return reddy(subs, LinkValueCast(vargs)->to_handle_seq());
		}

		if (SET_LINK != vtype)
		{
			return reddy(subs, {args});
		}

		// If the arguments are given in a set, then iterate over the set...
		HandleSeq bset;
		for (const Handle& h : args->getOutgoingSet())
		{
			HandleSeq oset;
			oset.emplace_back(h);
			try
			{
				bset.emplace_back(reddy(subs, oset));
			}
			catch (const TypeCheckException& ex) {}
		}
		return createLink(std::move(bset), SET_LINK);
	}

	// If we are here, then there are multiple variables in the body.

	// If the arguments are in a LinkValue, then assume that there is
	// only a single set of arguments to plug in. Same as LIST_LINK
	// below.
	if (nameserver().isA(vtype, LINK_VALUE))
	{
		HandleSeq argl(LinkValueCast(vargs)->to_handle_seq());

		HandleSeq oset;
		for (const Handle& h: argl)
			oset.push_back(expand(h, _silent));
		return reddy(subs, oset);
	}

	// If the arguments are in a ListLink, then assume that there is
	// only a single set of arguments to plug in.
	if (LIST_LINK == vtype)
	{
		HandleSeq oset;
		for (const Handle& h: args->getOutgoingSet())
			oset.push_back(expand(h, _silent));
		return reddy(subs, oset);
	}

	// If the argument is a LambdaLink, it will eta-reducible.
	// We already checked this earlier (a static check), so we
	// don't need any more checking. Just pass it through.
	if (LAMBDA_LINK == vtype)
	{
		return reddy(subs, {args});
	}

	// If we are here, then there are multiple arguments.
	// These MUST be given to us as a SetLink.
	if (SET_LINK != vtype)
	{
		if (_silent)
			throw TypeCheckException();

		throw RuntimeException(TRACE_INFO,
		                       "Should have caught this earlier, in the ctor");
	}

	HandleSeq bset;
	for (const Handle& h : args->getOutgoingSet())
	{
		const HandleSeq& oset = h->getOutgoingSet();
		try
		{
			bset.emplace_back(reddy(subs, oset));
		}
		catch (const TypeCheckException& ex) {}
	}
	return createLink(std::move(bset), SET_LINK);
}

static inline Handle do_exec(AtomSpace* as, bool silent, const Handle& h)
{
	Type t = h->get_type();
	if (not h->is_executable() or
	    nameserver().isA(t, VALUE_OF_LINK) or
	    nameserver().isA(t, SET_VALUE_LINK))
	{
		return h;
	}
	ValuePtr vex = h->execute(as, silent);
	return HandleCast(vex);
}

ValuePtr PutLink::execute(AtomSpace* as, bool silent)
{
	_silent = silent;

	Handle h(do_reduce());
	Type t = h->get_type();

	if ((SET_LINK == t) or (LIST_LINK == t))
	{
		HandleSeq oset;
		for (const Handle& ho : h->getOutgoingSet())
			oset.emplace_back(do_exec(as, silent, ho));
		return as->add_link(t, std::move(oset));
	}

	if (not h->is_executable() or
	    nameserver().isA(t, VALUE_OF_LINK) or
	    nameserver().isA(t, SET_VALUE_LINK))
	{
		return h;
	}
	return h->execute(as, silent);
}

// Crazy backwards-compatibility wrapper for PutLink.
// Normally, the result of executing PutLink is usually some Atom,
// the result of beta-reduction. If that Atom was also executable,
// then PutLink executes it, as a matter of "courtesy". This remains
// true, even now. But if the result of execution of a BoolValue,
// then cast it to a crisp true/false. This happens when the
// (PutLink (DefinedPredicate ...) ...) combination is used, which
// is tested in a few unit tests. This is a "crazy work-around"
// because normally PutLink is not (cannot be) an EVALUATABLE_LINK;
// but we "pretend" it is, by having ::is_evaluatable() return true.
// I guess we can live with this, but it doesn't follow the strict
// design rules used for everything else.
bool PutLink::bevaluate(AtomSpace* as, bool silent)
{
	ValuePtr boo(execute(as, silent));
	if (BOOL_VALUE != boo->get_type())
		throw RuntimeException(TRACE_INFO,
			"Abuse of old PutLink compatibility layer! %s beval to %s",
			to_string().c_str(), boo->to_string().c_str());

	BoolValuePtr bvp(BoolValueCast(boo));
	if (0 == bvp->size())
		throw RuntimeException(TRACE_INFO,
			"Abuse of old PutLink compatibility layer! bad size for %s",
			to_string().c_str());

	return bvp->value()[0];
}

DEFINE_LINK_FACTORY(PutLink, PUT_LINK)

/* ===================== END OF FILE ===================== */