#region Copyright
// --------------------------------------------------------------------------------------------------------------------
//
// Copyright (C) 2015 Ian Horswill
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in the
// Software without restriction, including without limitation the rights to use, copy,
// modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
// and to permit persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
// --------------------------------------------------------------------------------------------------------------------
#endregion
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Prolog
{
internal static class ELProlog
{
public const string NonExclusiveOperator = "/";
public static readonly Symbol SNonExclusiveOperator = Symbol.Intern(NonExclusiveOperator);
public const string ExclusiveOperator = ":";
public static readonly Symbol SExclusiveOperator = Symbol.Intern(ExclusiveOperator);
public const string BindNodeOperator = ">>";
public static readonly Symbol SBindNodeOperator = Symbol.Intern(BindNodeOperator);
public static bool IsELTerm(object term)
{
var s = term as Structure;
return s != null
&& s.Arity==2
&& (s.Functor == SBindNodeOperator
|| s.Functor == SExclusiveOperator
|| s.Functor == SNonExclusiveOperator);
}
#region Queries
public static bool TryQuery(object term, PrologContext context, out ELNode foundNode, out ELNodeEnumerator enumerator)
{
// Dereference any top-level variables.
var t = Term.Deref(term);
// Dereference indexicals
var i = t as Indexical;
if (i != null)
t = i.GetValue(context);
// A game object means the gameobject's EL KB.
var g = t as GameObject;
if (g != null)
t = g.KnowledgeBase().ELRoot;
// If it's already an ELNode, use that.
var n = t as ELNode;
if (n != null)
{
foundNode = n;
enumerator = null;
return true;
}
// Otherwise, it's an expression, so evaluate it.
var s = t as Structure;
if (s != null)
return TryQueryStructure(s, context, out foundNode, out enumerator);
var v = t as LogicVariable;
if (v != null && !v.IsBound)
throw new Exception("EL query root is an unbound variable: " + v);
throw new Exception("Malformed EL query: " + ISOPrologWriter.WriteToString(term));
}
public static bool TryQueryStructure(
Structure term,
PrologContext context,
out ELNode foundNode,
out ELNodeEnumerator enumerator)
{
//
// Dispatch based on the functor and arity.
//
// Handle root queries, i.e. /Key
if (term.IsFunctor(Symbol.Slash, 1))
return TryRootQuery(term, context, out foundNode, out enumerator);
if (!IsELTerm(term))
throw new Exception("Malformed EL query: " + ISOPrologWriter.WriteToString(term));
if (term.IsFunctor(SBindNodeOperator, 2))
{
var variableToBind = term.Argument(1) as LogicVariable;
if (variableToBind == null)
throw new ArgumentException("RHS of >> must be an uninstantiated variable: "+ ISOPrologWriter.WriteToString(term.Argument(1)));
foundNode = null;
return TryNodeBindingQuery(out enumerator, term.Argument(0), variableToBind, context);
}
return TryChildQuery(
out foundNode,
out enumerator,
term.Argument(0),
term.Argument(1),
term.Functor == Symbol.Colon,
context);
}
public static bool TryNodeBindingQuery(
out ELNodeEnumerator enumerator,
object nodeExpression,
LogicVariable variableToBind,
PrologContext context)
{
// Decode the node expression
ELNode foundNode;
ELNodeEnumerator nodeEnumerator;
if (!TryQuery(nodeExpression, context, out foundNode, out nodeEnumerator))
{
// Parent failed, so we fail
enumerator = null;
return false;
}
enumerator = (foundNode != null)
? (ELNodeEnumerator)new ELNodeEnumeratorBindFixedNodeToVariable(foundNode, variableToBind)
: new ELNodeEnumeratorBindEnumeratedNodesToVariable(nodeEnumerator, variableToBind);
return true;
}
///
/// Binds a specific node rather than a key (i.e. >> rather than / or :) to a variable
///
class ELNodeEnumeratorBindFixedNodeToVariable : ELNodeEnumerator
{
private readonly ELNode node;
private readonly LogicVariable variableToBind;
public ELNodeEnumeratorBindFixedNodeToVariable(ELNode node, LogicVariable variableToBind)
{
this.node = node;
this.variableToBind = variableToBind;
}
public override bool BindsVar(LogicVariable v)
{
return v == variableToBind;
}
public override bool MoveNext()
{
if (Current == null)
{
// first time through
variableToBind.Value = Current = node;
return true;
}
variableToBind.ForciblyUnbind();
return false;
}
}
///
/// Binds a set of nodes rather than keys (i.e. >> rather than / or :) to a variable
///
class ELNodeEnumeratorBindEnumeratedNodesToVariable : ELNodeEnumerator
{
readonly ELNodeEnumerator nodeEnumerator;
readonly LogicVariable variableToBind;
public ELNodeEnumeratorBindEnumeratedNodesToVariable(ELNodeEnumerator nodeEnumerator, LogicVariable variableToBind)
{
this.nodeEnumerator = nodeEnumerator;
this.variableToBind = variableToBind;
if (nodeEnumerator.BindsVar(variableToBind))
throw new InvalidOperationException("Variable appears on both the LHS and RHS of >>: "+ variableToBind.Name);
}
public override bool BindsVar(LogicVariable v)
{
return v == variableToBind || nodeEnumerator.BindsVar(v);
}
public override bool MoveNext()
{
if (nodeEnumerator.MoveNext())
{
Current = nodeEnumerator.Current;
variableToBind.Value = Current;
return true;
}
variableToBind.ForciblyUnbind();
return false;
}
}
public static bool TryChildQuery(
out ELNode foundNode,
out ELNodeEnumerator enumerator,
object parentExpression,
object keyExpression,
bool isExclusive,
PrologContext context)
{
// Decode the parent expression
ELNode parentNode;
ELNodeEnumerator parentEnumerator;
if (!TryQuery(parentExpression, context, out parentNode, out parentEnumerator))
{
// Parent failed, so we fail
enumerator = null;
foundNode = null;
return false;
}
//
// Decode the key argument
//
var key = keyExpression;
var v = key as LogicVariable;
return isExclusive?TryExclusiveQuery(out foundNode, out enumerator, parentNode, parentEnumerator, key, v)
: TryNonExclusiveQuery(out foundNode, out enumerator, parentNode, key, v, parentEnumerator);
}
private static bool TryExclusiveQuery(
out ELNode foundNode,
out ELNodeEnumerator enumerator,
ELNode parentNode,
ELNodeEnumerator parentEnumerator,
object key,
LogicVariable v)
{
//
// Expression is Parent:Something
//
if (parentNode != null)
{
return TryExclusiveQueryDeterministicParent(out foundNode, out enumerator, parentNode, key, v);
}
return TryExclusiveQueryEnumeratedParent(out foundNode, out enumerator, parentEnumerator, key, v);
}
private static bool TryExclusiveQueryEnumeratedParent(
out ELNode foundNode,
out ELNodeEnumerator enumerator,
ELNodeEnumerator parentEnumerator,
object key,
LogicVariable v)
{
// Non-deterministic parent path
// NonUniqueParent:Something
foundNode = null;
enumerator = (v == null)
? new ELNodeEnumeratorEnumerateParentAndLookupExclusiveKey(parentEnumerator, key)
: (parentEnumerator.BindsVar(v)? (ELNodeEnumerator)new ELNodeEnumeratorPreboundVariable(parentEnumerator, v, true)
: new ELNodeEnumeratorEnumerateParentAndBindVariable(parentEnumerator, v));
return true;
}
private static bool TryExclusiveQueryDeterministicParent(
out ELNode foundNode,
out ELNodeEnumerator enumerator,
ELNode parentNode,
object key,
LogicVariable v)
{
// Deterministic parent path
// UniqueParent:Something
if (parentNode.IsNonExclusive)
{
throw new ELNodeExclusionException("Exclusive query of an non-exclusive node", parentNode, key);
}
if (v == null)
{
// Deterministic child path
// UniqueParent:Key
enumerator = null;
return parentNode.TryLookup(key, out foundNode);
}
// Enumerated child path
// UniqueParent:Variable
if (parentNode.Children.Count > 0)
{
foundNode = null;
enumerator = new ELNodeEnumeratorBindAndUnbindVariable(parentNode.Children[0], v);
return true;
}
// parentNode is exclusive, but is childless, so we can't match.
foundNode = null;
enumerator = null;
return false;
}
private static bool TryNonExclusiveQuery(out ELNode foundNode, out ELNodeEnumerator enumerator, ELNode parentNode, object key, LogicVariable v, ELNodeEnumerator parentEnumerator)
{
if (parentNode != null)
{
return TryNonExclusiveQueryDeterministicParent(out foundNode, out enumerator, parentNode, key, v);
}
return TryNonExclusiveQueryEnumeratedParent(out foundNode, out enumerator, key, v, parentEnumerator);
}
private static bool TryNonExclusiveQueryEnumeratedParent(
out ELNode foundNode,
out ELNodeEnumerator enumerator,
object key,
LogicVariable v,
ELNodeEnumerator parentEnumerator)
{
// Enumerated parent path
// NonUniqueParent/Something
foundNode = null;
if (v == null)
{
// NonUniqueParent/Key
// Enumerate parent, then do deterministic lookup for child.
enumerator = new ELNodeEnumeratorFixedChildFromParentEnumerator(parentEnumerator, key);
return true;
}
if (parentEnumerator.BindsVar(v))
{
// We're doing a search for a variable that's aready bound.
enumerator = new ELNodeEnumeratorPreboundVariable(parentEnumerator, v, false);
return true;
}
// NonUniqueParent/Variable
// Enumerate both parent and child.
enumerator = new ELNodeEnumeratorLogicVariableFromParentEnumerator(parentEnumerator, v);
return true;
}
private static bool TryNonExclusiveQueryDeterministicParent(
out ELNode foundNode,
out ELNodeEnumerator enumerator,
ELNode parentNode,
object key,
LogicVariable v)
{
// Deterministic parent path
// The expression is UniqueParent/Something
if (parentNode.IsExclusive)
{
throw new ELNodeExclusionException("Non-exclusive query of an exclusive node", parentNode, key);
}
if (v == null)
{
// fully deterministic path
// UniqueParent/Key corresponds to at most one ELNode.
enumerator = null;
return parentNode.TryLookup(key, out foundNode);
}
// UniqueParent/Variable, so do a singly-nested iteration.
foundNode = null;
enumerator = new ELNodeEnumeratorLogicVariableFromNode(parentNode, v);
return true;
}
private static bool TryRootQuery(Structure term, PrologContext context, out ELNode foundNode, out ELNodeEnumerator enumerator)
{
// Expression is /Key.
var arg0 = term.Argument(0);
// This is a "/constant" expression, i.e. a top-level lookup.
if (arg0 is LogicVariable)
{
throw new NotImplementedException("Lookups of the form /Variable are not supported.");
}
enumerator = null;
return context.KnowledgeBase.ELRoot.TryLookup(arg0, out foundNode);
}
///
/// Enumerate the nodes whose keys are the same as the value in a (prebound) variable.
///
class ELNodeEnumeratorPreboundVariable : ELNodeEnumerator
{
public ELNodeEnumeratorPreboundVariable(ELNodeEnumerator parentEnumerator, LogicVariable variable, bool exclusive)
{
this.parentEnumerator = parentEnumerator;
this.variable = variable;
this.exclusive = exclusive;
}
private readonly ELNodeEnumerator parentEnumerator;
private readonly LogicVariable variable;
private readonly bool exclusive;
public override bool MoveNext()
{
while (parentEnumerator.MoveNext())
{
if (exclusive)
{
if (parentEnumerator.Current.IsNonExclusive)
throw new ELNodeExclusionException("Exclusive query of an non-exclusive node", parentEnumerator.Current, variable.Value);
}
else if (parentEnumerator.Current.IsExclusive)
throw new ELNodeExclusionException("Non-exclusive query of an exclusive node", parentEnumerator.Current, variable.Value);
foreach (var c in parentEnumerator.Current.Children)
{
if (c.Key.Equals(variable.Value))
{
Current = c;
return true;
}
}
}
return false;
}
public override bool BindsVar(LogicVariable v)
{
return parentEnumerator.BindsVar(v);
}
}
///
/// Enumerate the (non-exclusive) children of a node and bind their keys to a logic variable.
///
class ELNodeEnumeratorLogicVariableFromNode : ELNodeEnumerator
{
public ELNodeEnumeratorLogicVariableFromNode(ELNode parentNode, LogicVariable v)
{
this.parentNode = parentNode;
this.variable = v;
childIndex = 0;
}
private int childIndex;
private readonly ELNode parentNode;
private readonly LogicVariable variable;
public override bool MoveNext()
{
if (this.childIndex < parentNode.Children.Count)
{
Current = parentNode.Children[this.childIndex++];
this.variable.Value = Term.CopyInstantiation(Current.Key);
return true;
}
this.variable.ForciblyUnbind();
return false;
}
public override bool BindsVar(LogicVariable v)
{
return v == this.variable;
}
}
///
/// For each node enumerated by the parent, find its unique child with the specified key, if any.
///
class ELNodeEnumeratorFixedChildFromParentEnumerator : ELNodeEnumerator
{
public ELNodeEnumeratorFixedChildFromParentEnumerator(ELNodeEnumerator parentEnumerator, object childKey)
{
this.parentEnumerator = parentEnumerator;
this.childKey = childKey;
}
private readonly ELNodeEnumerator parentEnumerator;
private readonly object childKey;
public override bool MoveNext()
{
while (parentEnumerator.MoveNext())
{
// ReSharper disable once PossibleNullReferenceException
if (parentEnumerator.Current.IsExclusive)
throw new ELNodeExclusionException("Non-exclusive query of an exclusive node", parentEnumerator.Current, childKey);
// ReSharper disable once PossibleNullReferenceException
if (parentEnumerator.Current.TryLookup(childKey, out Current))
return true;
}
return false;
}
public override bool BindsVar(LogicVariable v)
{
return parentEnumerator.BindsVar(v);
}
}
///
/// For each node enumerated by the parent, enumerate all its child nodes,
/// and bind a LogicVariable to their keys.
///
class ELNodeEnumeratorLogicVariableFromParentEnumerator : ELNodeEnumerator
{
public ELNodeEnumeratorLogicVariableFromParentEnumerator(ELNodeEnumerator parentEnumerator, LogicVariable v)
{
this.parentEnumerator = parentEnumerator;
this.variable = v;
childIndex = -1;
}
private readonly ELNodeEnumerator parentEnumerator;
private readonly LogicVariable variable;
private int childIndex;
public override bool MoveNext()
{
retry:
// First, try the next child of the current parent.
if (childIndex >= 0)
{
Current = parentEnumerator.Current.Children[childIndex--];
this.variable.Value = Term.CopyInstantiation(Current.Key);
return true;
}
// Ran out of children on the current parent.
if (parentEnumerator.MoveNext())
{
// ReSharper disable once PossibleNullReferenceException
if (parentEnumerator.Current.IsExclusive)
throw new ELNodeExclusionException(
"Non-exclusive query of an exclusive node",
parentEnumerator.Current,
this.variable);
childIndex = parentEnumerator.Current.Children.Count - 1;
goto retry;
}
this.variable.ForciblyUnbind();
return false;
}
public override bool BindsVar(LogicVariable v)
{
return v == this.variable || parentEnumerator.BindsVar(v);
}
}
///
/// This doesn't really enumerate children, since there's only a single, fixed child.
/// But we structure it as an enumerator so that we get a callback after the one child
/// is processed, and that lets us unbind the variable we bound.
///
class ELNodeEnumeratorBindAndUnbindVariable : ELNodeEnumerator
{
public ELNodeEnumeratorBindAndUnbindVariable(ELNode child, LogicVariable v)
{
this.child = child;
this.variable = v;
}
private readonly ELNode child;
private readonly LogicVariable variable;
public override bool MoveNext()
{
if (this.variable.IsBound)
{
// We've already been through it once, so unbind the variable and fail.
this.variable.ForciblyUnbind();
return false;
}
// This is our first time through, so bind the variable and succeed.
Current = child;
this.variable.Value = Term.CopyInstantiation(child.Key);
return true;
}
public override bool BindsVar(LogicVariable v)
{
return v == this.variable;
}
}
///
/// For each node enumerated by parent, check if it has a specific key.
///
class ELNodeEnumeratorEnumerateParentAndLookupExclusiveKey : ELNodeEnumerator
{
public ELNodeEnumeratorEnumerateParentAndLookupExclusiveKey(ELNodeEnumerator parentEnumerator, object key)
{
this.parentEnumerator = parentEnumerator;
this.key = key;
}
private readonly ELNodeEnumerator parentEnumerator;
private readonly object key;
public override bool MoveNext()
{
while (parentEnumerator.MoveNext())
{
if (parentEnumerator.Current.IsNonExclusive)
{
throw new ELNodeExclusionException("Exclusive query of an non-exclusive node", parentEnumerator.Current, key);
}
if (parentEnumerator.Current.TryLookup(key, out Current))
return true;
}
return false;
}
public override bool BindsVar(LogicVariable v)
{
return parentEnumerator.BindsVar(v);
}
}
///
/// For each node enumerated by parent, bind LogicVariable to its unique child
///
class ELNodeEnumeratorEnumerateParentAndBindVariable : ELNodeEnumerator
{
public ELNodeEnumeratorEnumerateParentAndBindVariable(ELNodeEnumerator parentEnumerator, LogicVariable variable)
{
this.parentEnumerator = parentEnumerator;
this.variable = variable;
}
private readonly ELNodeEnumerator parentEnumerator;
private readonly LogicVariable variable;
public override bool MoveNext()
{
while (parentEnumerator.MoveNext())
{
if (parentEnumerator.Current.IsNonExclusive)
{
throw new ELNodeExclusionException("Exclusive query of an non-exclusive node", parentEnumerator.Current, this.variable);
}
if (parentEnumerator.Current.Children.Count > 0)
{
Current = parentEnumerator.Current.Children[0];
this.variable.Value = Term.CopyInstantiation(Current.Key);
return true;
}
}
this.variable.ForciblyUnbind();
return false;
}
public override bool BindsVar(LogicVariable v)
{
return v == this.variable || parentEnumerator.BindsVar(v);
}
}
#endregion
#region Assertion
//
// these write a single nodes, so they don't need to loop like queries do.
//
///
/// Write TERM to EL KB, creating any nodes that need to be cerated.
///
/// Prolog-format term to store into KB
/// KB in which to assert the term.
///
public static ELNode Update(object term, KnowledgeBase knowledgeBase)
{
term = Term.Deref(term);
var s = term as Structure;
if (s != null)
return UpdateStructure(s, knowledgeBase);
var n = term as ELNode;
if (n != null)
return n;
throw new Exception("Malformed EL assertion: " + ISOPrologWriter.WriteToString(term));
}
public static ELNode UpdateStructure(Structure term, KnowledgeBase knowledgeBase)
{
if (term.Functor == Symbol.Slash)
{
if (term.Arity == 1)
return knowledgeBase.ELRoot.StoreNonExclusive(Term.CopyInstantiation(term.Argument(0)));
return Update(term.Argument(0), knowledgeBase).StoreNonExclusive(Term.CopyInstantiation(term.Argument(1)));
}
if (term.Functor == Symbol.Colon)
{
return Update(term.Argument(0), knowledgeBase).StoreExclusive(Term.CopyInstantiation(term.Argument(1)), true);
}
throw new Exception("Malformed EL assertion: "+ISOPrologWriter.WriteToString(term));
}
#endregion
#region Retraction
internal static IEnumerable Retract(object term, PrologContext context)
{
ELNode foundNode;
ELNodeEnumerator enumerator;
if (!TryQuery(term, context, out foundNode, out enumerator))
return CutStateSequencer.Fail();
if (foundNode != null)
{
foundNode.DeleteSelf();
return CutStateSequencer.Succeed();
}
return DeleteSuccessive(enumerator);
}
private static IEnumerable DeleteSuccessive(ELNodeEnumerator enumerator)
{
while (enumerator.MoveNext())
{
enumerator.Current.DeleteSelf();
yield return CutState.Continue;
}
}
internal static void RetractAll(object term, PrologContext context)
{
ELNode foundNode;
ELNodeEnumerator enumerator;
if (!TryQuery(term, context, out foundNode, out enumerator))
return;
if (foundNode != null)
foundNode.DeleteSelf();
else
while (enumerator.MoveNext()) enumerator.Current.DeleteSelf();
}
#endregion
}
}