#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 } }