#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;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
using UnityEngine;
namespace Prolog
{
///
/// Controls the execution of a prolog program.
///
[DebuggerDisplay("Top={GoalStackTopSafe}")]
public class PrologContext : IDisposable
{
#region Static variables
///
/// The last PrologContext that threw an exception.
///
public static PrologContext LastExceptionContext { get; set; }
///
/// Default maximum number of steps the inference can run for
///
public static int DefaultStepLimit { get; set; }
#endregion
#region Instance variables
///
/// The KB to use for inference.
///
[SuppressMessage("Microsoft.Naming", "CA1702:CompoundWordsShouldBeCasedCorrectly", MessageId = "KnowledgeBase")]
public KnowledgeBase KnowledgeBase { get; internal set; }
///
/// The defaultstream to write output to.
///
public TextWriter Output;
///
/// The GameObject or Component responsible for initiating the current top-level goal.
///
public object This;
///
/// The GameObject responsible for initiating the current top-level goal.
///
public GameObject GameObject
{
get
{
return KnowledgeBase.GameObject;
}
}
public readonly List> IndexicalBindingStack;
///
/// Whether predicates should be randomized.
///
public bool Randomize { get; set; }
#endregion
#region Constructors
///
/// Creates a PrologContext with PrologContext.DefaultStepLimit
///
private PrologContext(KnowledgeBase kb)
: this(kb, DefaultStepLimit)
{ }
///
/// Creates a PrologContext that can for at most the specified number of steps.
///
public PrologContext(KnowledgeBase kb, int stepLimit)
{
StepsRemaining = stepLimit;
goalStack = new List();
goalStackCurrentRules = new List();
GoalStackDepth = 0;
KnowledgeBase = kb;
trailVariables = new LogicVariable[256];
trailValues = new object[256];
trailPointer = 0;
IndexicalBindingStack = new List>();
isFree = true;
}
static PrologContext()
{
DefaultStepLimit = 5000;
}
#endregion
#region Management of the free context pool
///
/// Pool of contexts available to be re-used.
///
static readonly StoragePool Pool = new StoragePool(() => new PrologContext(null));
///
/// True if this context is NOT supposed to be in use.
///
private bool isFree;
///
/// Gets a context that is currently free to use. Should be relased afterward using ReleaseContext().
///
/// A free PrologContext
public static PrologContext Allocate(KnowledgeBase kb, object thisValue)
{
var c = Pool.Allocate();
if (!c.isFree)
throw new InvalidOperationException("Allocated PrologContext is still in use!");
c.isFree = false;
c.KnowledgeBase = kb;
c.Reset(thisValue);
c.Output = Console.Out;
return c;
}
public void Dispose()
{
isFree = true;
Pool.Deallocate(this);
}
#endregion
#region Inference step counting
private int stepLimit;
///
/// Default maximum number of steps the inference can run for
///
public int StepLimit
{
get { return stepLimit; }
set
{
int delta = value - stepLimit;
stepLimit = value;
StepsRemaining += delta;
}
}
///
/// Number of further steps this inference is allowed to run for.
///
public int StepsRemaining { get; set; }
///
/// Number of inference steps (calls to Prove) performed
///
public int StepsUsed
{
get { return StepLimit - StepsRemaining; }
}
///
/// Check whether the maximum number of steps has been exceeded.
/// Called when a new step is initiated.
///
internal void NewStep() {
StepsRemaining -= 1;
if (StepsRemaining < 0)
throw new InferenceStepsExceededException(this);
}
#endregion
#region Proving goals
///
/// Resets the context (clears stack, etc.) and starts a proof of the specified goal.
///
/// Goal to attempt to prove
/// Enumerator for solutions
public IEnumerable ResetStackAndProve(Structure goal)
{
Reset(this.This);
foreach (var state in KnowledgeBase.Prove(goal.Functor, goal.Arguments, this, 0))
if (state == CutState.ForceFail)
yield break;
else
yield return false;
}
///
/// Resets the context (clears stack, etc.) and starts a proof of the specified goal.
///
/// Goal to attempt to prove
/// Enumerator for solutions
public IEnumerable ResetStackAndProve(object goal)
{
Reset(this.This);
Structure s = Term.Structurify(goal, "Invalid goal.");
foreach (var state in KnowledgeBase.Prove(s.Functor, s.Arguments, this, 0))
if (state == CutState.ForceFail)
yield break;
else
yield return false;
}
///
/// Test whether GOAL is provable
///
/// Goal to prove
/// True if provable, else false.
public bool IsTrue(Structure goal)
{
return Prove(goal).GetEnumerator().MoveNext();
}
///
/// Test whether GOAL is provable
///
/// Name of the predicate to call
/// Arguments to the predicate, if any.
/// True if provable, else false.
public bool IsTrue(string predicateName, params object[] predicateArgs)
{
return this.IsTrue(new Structure(predicateName, predicateArgs));
}
///
/// Proves the goal in the specified structure.
///
internal IEnumerable Prove(Structure goal)
{
return KnowledgeBase.Prove(goal.Functor, goal.Arguments, this, CurrentFrame);
}
///
/// Proves the specified goal, throwing an exception with badGoalErrorMessage if the goal is ill-formed.
///
internal IEnumerable Prove(object goal, string badGoalErrorMessage)
{
Structure s = Term.Structurify(goal, badGoalErrorMessage);
return KnowledgeBase.Prove(s.Functor, s.Arguments, this, CurrentFrame);
}
#endregion
#region Tracing goals (not in the sense of the undo stack)
///
/// Prints trace information.
///
public void TraceOutput(string format, object arg)
{
ushort frame = CurrentFrame;
while (frame != 0)
{
Prolog.TraceOutput.Write(' ');
frame = goalStack[frame].Parent;
}
Prolog.TraceOutput.WriteLine(format, arg);
}
#endregion
#region Goal stack operations
///
/// Number of goals on the stack, meaning it's also the index at which to store the NEXT new subgoal.
///
public ushort GoalStackDepth { get; private set; }
///
/// The stackaddress of the stack frame for the currently running operation.
/// This should be GoalStackDepth-1.
///
public ushort CurrentFrame
{
get
{
return (ushort)(GoalStackDepth - 1);
}
}
private struct GoalStackFrame
{
///
/// The frame of the caller of this goal.
///
public readonly ushort Parent;
///
/// The functor of this goal
///
public readonly Symbol Functor;
///
/// The arguments to this goal
///
public readonly object[] Arguments;
public GoalStackFrame(Symbol functor, object[] args, ushort parentFrame)
{
this.Functor = functor;
this.Arguments = args;
this.Parent = parentFrame;
}
}
///
/// Stack frames for running goals.
/// Kept as a struct to keep the GC from having to copy a lot of objects.
/// Current rule for each stack frame is kept in a separate array because of issues with struct semantics.
///
private readonly List goalStack;
///
/// This has to be a separate parallel array to goalStack because while the rest of the stack frame
/// is immutable (within a given call), the rule changes during the call, and the CLR doesn't like
/// it when you try to mutate one field of one element of an array of structs.
///
private readonly List goalStackCurrentRules;
///
/// Returns the goal at the specified position on the stack
///
public Structure GoalStackGoal(ushort frame)
{
return new Structure(goalStack[frame].Functor, goalStack[frame].Arguments);
}
///
/// Returns the stack position of the parent goal of the goal at the specified position on the stack
///
public ushort GoalStackParent(ushort frame)
{
return goalStack[frame].Parent;
}
///
/// The goal at the top of the stack (i.e. the most recent/most-recursed subgoal).
///
[SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "GoalStackTop")]
public Structure GoalStackTop
{
get
{
return GetStackTop(true);
}
}
[SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
// ReSharper disable UnusedMember.Local
Structure GoalStackTopSafe
// ReSharper restore UnusedMember.Local
{
get
{
return GetStackTop(false);
}
}
[SuppressMessage("Microsoft.Naming", "CA2204:Literals should be spelled correctly", MessageId = "GoalStackTop")]
private Structure GetStackTop(bool throwOnEmptyStack)
{
if (GoalStackDepth == 0)
{
if (throwOnEmptyStack)
throw new InvalidOperationException("GoalStackTop: Goal stack is empty");
return null;
}
return this.GoalStackGoal(CurrentFrame);
//var index = CurrentFrame;
//if (goalStack[index].Functor == null)
// return new Structure(goalStack[index-1].Functor, goalStackArguments[index-1]);
//return new Structure(goalStackFunctor[index], goalStackArguments[index]);
}
public void Reset()
{
this.Reset(this.This);
}
///
/// Forcibly clears the execution context.
///
public void Reset(object thisValue)
{
GoalStackDepth = 0;
dataStackPointer = 0;
Randomize = false;
if (wokenStack != null)
wokenStack.Clear();
StepsRemaining = StepLimit = DefaultStepLimit;
this.This = thisValue;
IndexicalBindingStack.Clear();
}
///
/// Renews the step limit (e.g. for when the repl is asking for a new solution.
///
public void ResetStepLimit()
{
StepsRemaining = DefaultStepLimit;
}
///
/// Adds a new goal to the goal stack
///
[SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "functor")]
public void PushGoalStack(Symbol functor, object[] args, ushort parentFrame)
{
if (GoalStackDepth >= goalStack.Count)
{
goalStack.Add(new GoalStackFrame(functor, args, parentFrame));
goalStackCurrentRules.Add(null);
}
else
{
goalStack[GoalStackDepth] = new GoalStackFrame(functor, args, parentFrame);
goalStackCurrentRules[GoalStackDepth] = null;
}
GoalStackDepth++;
}
///
/// Mark that this is the start of a new clause
///
//public ushort PushClause()
//{
// PushGoalStack(null, null, 0);
// return (ushort)(GoalStackDepth - 2);
//}
///
/// Removes the top goal from the goal stack
///
public void PopGoalStack()
{
GoalStackDepth = (ushort)(GoalStackDepth - 1);
}
///
/// Resets the stack to the specified depth.
///
public void UnwindStack(ushort depth)
{
GoalStackDepth = depth;
}
///
/// Set the rule currently being tried by the goal at the top of the stack.
///
/// the new rule.
public void SetCurrentRule(KnowledgeBaseEntry rule)
{
goalStackCurrentRules[CurrentFrame] = rule;
}
///
/// Generate a stack trace that's close enough to a normal mono stack dump that the Unity logger will understand it.
///
/// Path for the source file being loaded.
/// Current line number in the source file.
/// Original prolog command to output, if stack is empty.
/// If true, the complete stack is dumped, otherwise, just the starting frames.
///
public string StackTrace(string sourcePath, int lineNumber, string toplevelCommand, bool fullTrace)
{
ushort startingFrame = fullTrace ? this.CurrentFrame : (ushort)Math.Min((int)this.CurrentFrame, 30);
var result = new StringBuilder();
if (this.GoalStackDepth > 0)
for (int i = startingFrame; i >= 0; i--)
{
Structure g = this.GoalStackGoal((ushort)i);
if (g != null)
{
var frame = (ushort)i;
while (frame != 0)
{
//Output.Write("{0}/", frame);
result.Append(" ");
frame = this.GoalStackParent(frame);
}
//Output.Write(' ');
//Output.Write("{0}<{1}: ", i, PrologContext.GoalStackParent(i));
result.Append(Term.ToStringInPrologFormat(g));
var rule = goalStackCurrentRules[i];
if (rule != null)
result.AppendFormat(" (at {0}:{1})", rule.SourceFile, rule.SourceLineNumber);
result.AppendLine();
}
}
else
result.AppendFormat("{0} (at {1}:{2})\n", toplevelCommand, sourcePath, lineNumber);
return result.ToString();
}
#endregion
#region Trail (undo stack) operations
///
/// The position in the trace where the next spilled variable will be stored.
///
private int trailPointer;
private LogicVariable[] trailVariables;
private object[] trailValues;
///
/// Saves the current value of a variable on the trail (i.e. the undo stack).
///
public void SaveVariable(LogicVariable lvar)
{
if (trailPointer==trailVariables.Length)
{
var newTraceVariables = new LogicVariable[trailVariables.Length*2];
var newTraceValues = new object[trailVariables.Length*2];
trailVariables.CopyTo(newTraceVariables, 0);
trailValues.CopyTo(newTraceValues, 0);
trailVariables = newTraceVariables;
trailValues = newTraceValues;
}
trailVariables[trailPointer] = lvar;
trailValues[trailPointer] = lvar.mValue;
trailPointer++;
}
///
/// Restores the values of all variables back to the specified position on the trace (i.e. the undo stack).
///
public void RestoreVariables(int savedTrailPointer)
{
AbortWokenGoals(savedTrailPointer);
while (trailPointer != savedTrailPointer)
{
trailPointer--;
trailVariables[trailPointer].mValue = trailValues[trailPointer];
}
}
///
/// Marks a place on the trail so subsequent bindings can be undone.
/// Present implementation does not actually modify the stack in any way.
///
///
public int MarkTrail()
{
return trailPointer;
}
#endregion
#region Data stack operations
private object[] dataStack = new object[256];
private int dataStackPointer;
///
/// Reserve space for a new frame.
///
/// Number of words needed for frame
public int MakeFrame(int size)
{
int framePointer = dataStackPointer;
dataStackPointer += size;
if (dataStackPointer>dataStack.Length)
{
var newStack = new object[dataStack.Length*2+ByteCompiledRule.MaxArity];
dataStack.CopyTo(newStack,0);
dataStack = newStack;
}
return framePointer;
}
///
/// Resets stack pointer to point at base of old frame.
///
/// Base of the frame we're popping off
public void PopFrame(int framePointer)
{
dataStackPointer = framePointer;
}
///
/// Reads a value from the stack.
///
/// Base address of stack frame
/// Offset into stack frame
/// Value of stack entry
public object GetStack(int frame, int offset)
{
return dataStack[frame + offset];
}
///
/// Modifies a value from the stack.
///
/// Base address of stack frame
/// Offset into stack frame
/// New value for stack variable
public void SetStack(int frame, int offset, object value)
{
dataStack[frame + offset] = value;
}
///
/// Sets argument for an upcoming call.
///
/// Index of the argument (0=first, 1=second, etc.)
/// Value of the argument
public void SetCallArg(int argumentNumber, object value)
{
dataStack[dataStackPointer + argumentNumber] = value;
}
///
/// Copies args to a arguments position for the next stack frame.
///
internal void PushArguments(object[] args)
{
args.CopyTo(dataStack, dataStackPointer);
}
///
/// Copies args to a arguments position for the next stack frame.
///
internal void PushArguments(IList args)
{
args.CopyTo(dataStack, dataStackPointer);
}
internal object[] GetCallArgumentsAsArray(int arity)
{
var args = new object[arity];
Array.Copy(dataStack, dataStackPointer, args, 0, arity);
return args;
}
#endregion
#region Suspended goal stack
///
/// Represents a recently woken goal.
/// We keep a saved trace pointer so that if we have to abort the current unification before the goals are run, we can recognize which ones to remove.
///
private class WokenGoal
{
public int TracePointer;
public Structure Goal;
}
private Stack wokenStack;
internal void WakeUpGoal(Structure goal)
{
if (wokenStack == null)
wokenStack = new Stack();
wokenStack.Push(new WokenGoal { TracePointer = trailPointer, Goal = goal});
}
void AbortWokenGoals(int newTracePointer)
{
if (wokenStack == null)
return;
while (wokenStack.Count > 0 && wokenStack.Peek().TracePointer > newTracePointer)
wokenStack.Pop();
}
///
/// True if there are woken goals to process.
///
public bool GoalsHaveWoken
{
get { return wokenStack != null && wokenStack.Count > 0; }
}
///
/// Attempts to prove all woken goals, in the order they were woken.
///
///
internal IEnumerable ProveAllWokenGoals()
{
if (this.wokenStack == null || this.wokenStack.Count==0)
return CutStateSequencer.Succeed();
WokenGoal[] goals = wokenStack.ToArray();
wokenStack.Clear();
return ProveWokenGoalsInternal(goals, 0);
}
private IEnumerable ProveWokenGoalsInternal(WokenGoal[] goals, int goalIndex)
{
#pragma warning disable 168
// ReSharper disable UnusedVariable
foreach (var ignore in Prove(goals[goalIndex].Goal))
if (goalIndex < goals.Length - 1)
foreach (var ignore2 in ProveWokenGoalsInternal(goals, goalIndex + 1))
// ReSharper restore UnusedVariable
#pragma warning restore 168
yield return CutState.Continue;
else
yield return CutState.Continue;
}
#endregion
}
}