diff --git a/src/YesNt.Interpreter/Runtime/StepResult.cs b/src/YesNt.Interpreter/Runtime/StepResult.cs new file mode 100644 index 0000000..114dca7 --- /dev/null +++ b/src/YesNt.Interpreter/Runtime/StepResult.cs @@ -0,0 +1,27 @@ +namespace YesNt.Interpreter.Runtime; + +/// +/// The outcome of a single call, +/// or the aggregate result of / +/// . +/// +public enum StepResult +{ + /// + /// A line was executed and more lines remain. Keep stepping to continue. + /// + Continue, + + /// + /// The step or time budget was exhausted before the script finished. + /// is still ; + /// call any of the run methods again to resume. + /// + Paused, + + /// + /// The script ran to completion (end-of-file, explicit exit, or error). + /// is now . + /// + Finished, +} \ No newline at end of file diff --git a/src/YesNt.Interpreter/Runtime/YesNtInterpreter.cs b/src/YesNt.Interpreter/Runtime/YesNtInterpreter.cs index c7f3752..c7be90b 100644 --- a/src/YesNt.Interpreter/Runtime/YesNtInterpreter.cs +++ b/src/YesNt.Interpreter/Runtime/YesNtInterpreter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.IO; using System.Linq; @@ -59,6 +60,13 @@ public class YesNtInterpreter } } + /// + /// from the moment (or any + /// Execute overload) is called until the script finishes or is stopped. + /// Use this to drive step/run loops: while (interpreter.IsRunning) interpreter.Step(10); + /// + public bool IsRunning { get; private set; } + /// /// Initializes a new and registers all built-in statements. /// @@ -274,7 +282,53 @@ public class YesNtInterpreter } /// - /// Executes a YesNt script file. + /// Loads a YesNt script file and prepares it for stepped execution. + /// After this call is and you can drive + /// execution with , , or . + /// + /// The path to the .ynt script file. + /// + /// When , output is routed through instead of + /// and line-execution events are raised via . + /// + public void Prepare(string path, bool isDebugMode = false) + { + runtimeInfo.Reset(); + runtimeInfo.IsDebugMode = isDebugMode; + if (LoadFile(path)) + { + IsRunning = true; + } + } + + /// + /// Loads an in-memory script and prepares it for stepped execution. + /// After this call is and you can drive + /// execution with , , or . + /// + /// The script lines to load. + /// + /// When , output is routed through and + /// line-execution events are raised via . + /// + public void Prepare(IEnumerable lines, bool isDebugMode = false) + { + runtimeInfo.Reset(); + runtimeInfo.IsDebugMode = isDebugMode; + + int i = 0; + foreach (string line in lines) + { + string content = line.Trim().Replace("\r", string.Empty); + runtimeInfo.Lines.Add(new Line(content, "#Memory#", i++)); + } + + PreScanLines(); + IsRunning = true; + } + + /// + /// Executes a YesNt script file to completion. /// /// The path to the .ynt script file. /// @@ -283,35 +337,22 @@ public class YesNtInterpreter /// public void Execute(string path, bool isDebugMode = false) { - runtimeInfo.Reset(); - runtimeInfo.IsDebugMode = isDebugMode; - if (LoadFile(path)) - { - Execute(); - } + Prepare(path, isDebugMode); + RunToCompletion(); } /// - /// Executes a YesNt script supplied as an in-memory list of lines. + /// Executes a YesNt script supplied as an in-memory list of lines to completion. /// /// The script lines to execute. /// /// When , output is routed through and /// line-execution events are raised via . /// - public void Execute(List lines, bool isDebugMode = false) + public void Execute(IEnumerable lines, bool isDebugMode = false) { - runtimeInfo.Reset(); - runtimeInfo.IsDebugMode = isDebugMode; - - for (int i = 0; i < lines.Count; i++) - { - string content = lines[i].Trim().Replace("\r", string.Empty); - runtimeInfo.Lines.Add(new Line(content, Path.GetFileName("#Memory#"), i)); - } - - PreScanLines(); - Execute(); + Prepare(lines, isDebugMode); + RunToCompletion(); } internal void Execute(List lines, Dictionary globalVariables, int startLine, RuntimeInformation parentRuntimeInformation) @@ -322,22 +363,98 @@ public class YesNtInterpreter runtimeInfo.LineNumber = startLine; runtimeInfo.ParentRuntimeInformation = parentRuntimeInformation; runtimeInfo.GlobalVariables = globalVariables; + if (parentRuntimeInformation.StopAllTasks) { runtimeInfo.Exit(ExitMessages.TerminatedByParentTask, parentRuntimeInformation.StopAllTasks); return; } + PreScanLines(); - Execute(); + IsRunning = true; + RunToCompletion(); } - private void Execute() + /// + /// Executes up to script lines then pauses, leaving + /// so execution can be resumed later. + /// Blank lines and comments are skipped transparently and do not consume the budget. + /// + /// Maximum number of executable lines to run. Defaults to 1. + /// + /// if the budget was exhausted but the script is not finished; + /// if the script ended within the budget. + /// + public StepResult Step(int lines = 1) { - for (; runtimeInfo.LineNumber < runtimeInfo.Lines.Count; runtimeInfo.LineNumber++) + for (int i = 0; i < lines; i++) + { + StepResult result = StepOnce(); + if (result != StepResult.Continue) + { + return result; + } + } + + return StepResult.Paused; + } + + /// + /// Runs the script for up to of wall-clock time, then pauses. + /// The check happens between lines, so a single slow statement may overshoot slightly. + /// + /// How long to run before pausing. + /// + /// if the budget expired but the script is not finished; + /// if the script ended within the budget. + /// + public StepResult RunFor(TimeSpan budget) + { + if (!IsRunning) + { + return StepResult.Finished; + } + + Stopwatch sw = Stopwatch.StartNew(); + + while (sw.Elapsed < budget) + { + StepResult result = StepOnce(); + if (result != StepResult.Continue) + { + return result; + } + } + + return StepResult.Paused; + } + + /// + /// Runs the script to completion from the current position. + /// If the script has not been started yet (i.e. is ) + /// this method returns immediately. + /// + public void RunToCompletion() + { + while (IsRunning) + { + _ = StepOnce(); + } + } + + private StepResult StepOnce() + { + if (!IsRunning) + { + return StepResult.Finished; + } + + // Skip blank lines and comments without consuming the step budget. + while (runtimeInfo.LineNumber < runtimeInfo.Lines.Count) { if (runtimeInfo.Stop) { - break; + return FinishExecution(); } Line lineObj = runtimeInfo.Lines[runtimeInfo.LineNumber]; @@ -345,9 +462,11 @@ public class YesNtInterpreter if (string.IsNullOrWhiteSpace(runtimeInfo.CurrentLine) || runtimeInfo.CurrentLine.StartsWith('#')) { + runtimeInfo.LineNumber++; continue; } + // We have a real executable line — run it. DebugEventArgs debugEventArgs = null; if (runtimeInfo.IsDebugMode) { @@ -362,8 +481,7 @@ public class YesNtInterpreter foreach (KeyValuePair staticStatement in staticStatements) { - StaticStatementAttributeContainer staticStatementAttribute = staticStatement.Key; - if (!staticStatementAttribute.ExecuteInSearchMode && runtimeInfo.IsSearching) + if (!staticStatement.Key.ExecuteInSearchMode && runtimeInfo.IsSearching) { continue; } @@ -374,7 +492,9 @@ public class YesNtInterpreter bool statementFound = false; bool notSearchingLabel = !runtimeInfo.IsSearching; - List handlers = (runtimeInfo.LineNumber < lineMatchingHandlers.Count) ? lineMatchingHandlers[runtimeInfo.LineNumber] : []; + List handlers = (runtimeInfo.LineNumber < lineMatchingHandlers.Count) + ? lineMatchingHandlers[runtimeInfo.LineNumber] + : []; foreach (StatementHandler handler in handlers) { @@ -425,13 +545,27 @@ public class YesNtInterpreter { runtimeInfo.Exit(ExitMessages.InvalidStatement, true); } + if (runtimeInfo.IsDebugMode && notSearchingLabel && debugEventArgs != null) { debugEventArgs.CurrentLine = runtimeInfo.CurrentLine.FromSafeString(); runtimeInfo.LineExecuted(debugEventArgs); } + + runtimeInfo.LineNumber++; + + // A statement may have set Stop (e.g. an explicit exit keyword). + return runtimeInfo.Stop ? FinishExecution() : StepResult.Continue; } + // Fell off the end of the script. + return FinishExecution(); + } + + private StepResult FinishExecution() + { + IsRunning = false; + if (!runtimeInfo.Stop) { if (!string.IsNullOrWhiteSpace(runtimeInfo.SearchLabel)) @@ -446,12 +580,14 @@ public class YesNtInterpreter { runtimeInfo.Exit(ExitMessages.EndOfFile, false); } - - if (runtimeInfo.IsDebugMode) - { - runtimeInfo.LineExecuted(null); - } } + + if (runtimeInfo.IsDebugMode) + { + runtimeInfo.LineExecuted(null); + } + + return StepResult.Finished; } private bool LoadFile(string path)