diff --git a/YesNt.Interpreter.Tests/AddStatementTests.cs b/YesNt.Interpreter.Tests/AddStatementTests.cs index 770d2e4..f604839 100644 --- a/YesNt.Interpreter.Tests/AddStatementTests.cs +++ b/YesNt.Interpreter.Tests/AddStatementTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using YesNt.Interpreter.Attributes; using YesNt.Interpreter.Enums; +using YesNt.Interpreter.Runtime; namespace YesNt.Interpreter.Tests; @@ -333,4 +334,44 @@ public class AddStatementTests interpreter.AddStatement("var", SearchMode.StartOfLine, SpaceAround.End, _ => { }); }); } + + [TestMethod] + public void AddStatementWithRuntimeInfoCanSetVariableTest() + { + List lines = + [ + "set_var foo", + "${foo}" + ]; + + YesNtAssert.IsLastLineEqualWithSetup(lines, "hello", interpreter => + { + interpreter.AddStatement("set_var", SearchMode.StartOfLine, SpaceAround.End, (args, rt) => + { + rt.Variables[args] = "hello"; + }); + }); + } + + [TestMethod] + public void AddStatementWithRuntimeInfoCanReadVariableTest() + { + List lines = + [ + "var greeting = world", + "echo_var greeting" + ]; + + string? captured = null; + + YesNtAssert.GetLastLineWithSetup(lines, interpreter => + { + interpreter.AddStatement("echo_var", SearchMode.StartOfLine, SpaceAround.End, (args, rt) => + { + _ = rt.Variables.TryGetValue(args, out captured); + }); + }); + + Assert.AreEqual("world", captured); + } } \ No newline at end of file diff --git a/YesNt.Interpreter/Runtime/IStatementContext.cs b/YesNt.Interpreter/Runtime/IStatementContext.cs new file mode 100644 index 0000000..58569e8 --- /dev/null +++ b/YesNt.Interpreter/Runtime/IStatementContext.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace YesNt.Interpreter.Runtime; + +/// +/// Exposes the script runtime state accessible to custom statement handlers registered +/// via . +/// +public interface IStatementContext +{ + /// Gets the local variable table for the current scope. + Dictionary Variables { get; } + + /// Gets or sets the global variable table shared across all scopes. + Dictionary GlobalVariables { get; set; } + + /// Gets or sets the text of the line currently being processed. + /// Inline-substitution handlers (e.g. %read_line) write their result here. + string CurrentLine { get; set; } + + /// Gets or sets the zero-based index of the next line to execute. + /// Set this to implement control-flow jumps inside a custom statement. + int LineNumber { get; set; } + + /// Terminates execution with the given message. + /// The message written to debug output. + /// + /// to signal an error termination; + /// for a planned, non-error termination. + /// + void Exit(string message, bool isError); +} diff --git a/YesNt.Interpreter/Runtime/RuntimeInformation.cs b/YesNt.Interpreter/Runtime/RuntimeInformation.cs index 371ed94..0ac8db9 100644 --- a/YesNt.Interpreter/Runtime/RuntimeInformation.cs +++ b/YesNt.Interpreter/Runtime/RuntimeInformation.cs @@ -13,7 +13,7 @@ namespace YesNt.Interpreter.Runtime; /// whose points back /// to the main execution context. /// -internal sealed class RuntimeInformation +internal sealed class RuntimeInformation : IStatementContext { public event Action OnDebugOutput; diff --git a/YesNt.Interpreter/Runtime/YesNtInterpreter.cs b/YesNt.Interpreter/Runtime/YesNtInterpreter.cs index bb722e3..2b75594 100644 --- a/YesNt.Interpreter/Runtime/YesNtInterpreter.cs +++ b/YesNt.Interpreter/Runtime/YesNtInterpreter.cs @@ -111,6 +111,20 @@ public class YesNtInterpreter PreScanLines(); } + /// + /// Registers a custom statement using a pre-built , + /// with access to the script's (variables, line number, output, etc.). + /// + /// The attribute describing the keyword, search mode, and priority. + /// + /// The delegate invoked when the statement matches. Receives the argument text and the current + /// for reading/writing script state. + /// + public void AddStatement(StatementAttribute attribute, Action handler) + { + AddStatement(attribute, args => handler(args, runtimeInfo)); + } + /// /// Registers a simple custom statement with default settings. /// @@ -123,6 +137,22 @@ public class YesNtInterpreter AddStatement(new StatementAttribute(name, searchMode, spaceAround), handler); } + /// + /// Registers a simple custom statement with default settings, + /// with access to the script's (variables, line number, output, etc.). + /// + /// The keyword to match. + /// Where in the line the keyword is searched for. + /// Which sides of the keyword must be padded with a space. + /// + /// The delegate invoked when the statement matches. Receives the argument text and the current + /// for reading/writing script state. + /// + public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action handler) + { + AddStatement(new StatementAttribute(name, searchMode, spaceAround), handler); + } + /// /// Registers a simple custom statement with a specific syntax-highlight color. /// @@ -136,6 +166,23 @@ public class YesNtInterpreter AddStatement(new StatementAttribute(name, searchMode, spaceAround, consoleColor), handler); } + /// + /// Registers a simple custom statement with a specific syntax-highlight color, + /// with access to the script's (variables, line number, output, etc.). + /// + /// The keyword to match. + /// Where in the line the keyword is searched for. + /// Which sides of the keyword must be padded with a space. + /// The color used for syntax highlighting. + /// + /// The delegate invoked when the statement matches. Receives the argument text and the current + /// for reading/writing script state. + /// + public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor consoleColor, Action handler) + { + AddStatement(new StatementAttribute(name, searchMode, spaceAround, consoleColor), handler); + } + /// /// Unregisters all handlers matching the specified keyword . /// diff --git a/docs/library-api.md b/docs/library-api.md index caf9109..bb6fd1a 100644 --- a/docs/library-api.md +++ b/docs/library-api.md @@ -124,6 +124,37 @@ interpreter.AddStatement("log", SearchMode.StartOfLine, SpaceAround.End, args => interpreter.Execute(new List { "log Hello from custom statement" }); ``` +### Accessing script state from a handler + +Pass an `Action` instead of `Action` to receive the current +script state. `IStatementContext` exposes the variable tables, the current line, the line number, +and the ability to terminate execution. + +```csharp +using YesNt.Interpreter.Runtime; + +interpreter.AddStatement("set_var", SearchMode.StartOfLine, SpaceAround.End, + (args, ctx) => + { + // args is e.g. "result 42" — parse however your syntax demands + string[] parts = args.Split(' ', 2); + if (parts.Length == 2) + ctx.Variables[parts[0]] = parts[1]; + else + ctx.Exit("set_var requires: ", isError: true); + }); +``` + +`IStatementContext` provides: + +| Property | Type | Description | +| ------------------------ | --------------------------- | -------------------------------------------------------------------------- | +| `Variables` | `Dictionary` | Local variable table for the current scope | +| `GlobalVariables` | `Dictionary` | Global variable table shared across all scopes | +| `CurrentLine` | `string` | The line text being processed; write here for inline-substitution handlers | +| `LineNumber` | `int` | Zero-based index of the _next_ line to execute; set to implement jumps | +| `Exit(message, isError)` | `void` | Terminate execution; `isError: true` signals an error condition | + ### With a syntax-highlight colour ```csharp @@ -240,6 +271,9 @@ interpreter.AddStatement("exec", SearchMode.StartOfLine, SpaceAround.End, args = ## Stopping a script +Call `Stop()` from any thread to request graceful termination. The script stops at the next line +boundary (or immediately if it is currently blocked waiting for console input). + ```csharp var interpreter = new YesNtInterpreter(); @@ -253,6 +287,27 @@ interpreter.Stop(); // signals the script to terminate at the next line bounda thread.Join(); ``` +### Stopping a script that blocks on `%read_key` + +When a script blocks waiting for keyboard input, use the `OnWaitingForInput` event instead of a +fixed `Thread.Sleep`. The event fires at the exact moment the interpreter enters the blocking poll +loop, so calling `Stop()` immediately after is always safe regardless of system load. + +```csharp +var interpreter = new YesNtInterpreter(); +var waitingForInput = new System.Threading.AutoResetEvent(false); + +interpreter.OnWaitingForInput += () => waitingForInput.Set(); + +var thread = new System.Threading.Thread(() => + interpreter.Execute(new List { "var key = %read_key" })); + +thread.Start(); +waitingForInput.WaitOne(TimeSpan.FromSeconds(5)); // wait until blocked on input +interpreter.Stop(); +thread.Join(); +``` + --- ## Reading registered statements @@ -301,12 +356,15 @@ Creates a new interpreter instance and registers all built-in statements. #### Events ```csharp -public event Action OnDebugOutput; +public event Action OnDebugOutput; public event Action OnLineExecuted; +public event Action OnWaitingForInput; ``` -Only raised in debug mode (`isDebugMode: true`). +`OnDebugOutput` and `OnLineExecuted` are only raised in debug mode (`isDebugMode: true`). `OnLineExecuted` receives `null` only on EOF completion. +`OnWaitingForInput` is raised (in any mode) immediately before the interpreter blocks on +`%read_key`. Use it to call `Stop()` deterministically without relying on `Thread.Sleep`. #### Methods @@ -319,10 +377,13 @@ public void Execute(List lines, bool isDebugMode = false); // Register a custom statement (full control) public void AddStatement(StatementAttribute attribute, Action handler); +public void AddStatement(StatementAttribute attribute, Action handler); // Register a custom statement (convenience overloads) public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action handler); +public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action handler); public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor color, Action handler); +public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor color, Action handler); // Remove a built-in or custom statement permanently public void RemoveStatement(string name); @@ -343,3 +404,22 @@ public void Stop(); // Read-only snapshot of all registered statements public ReadOnlyCollection StatementInformation { get; } ``` + +--- + +### `IStatementContext` + +```csharp +public interface IStatementContext // YesNt.Interpreter.Runtime +``` + +Passed to `Action` handlers registered via `AddStatement`. +Provides access to the script state that a built-in statement handler would have. + +| Member | Type | Description | +| ------------------------ | --------------------------- | ------------------------------------------------------------------------- | +| `Variables` | `Dictionary` | Local variable table for the current scope | +| `GlobalVariables` | `Dictionary` | Global variable table shared across all scopes | +| `CurrentLine` | `string` | The line being processed; write here for inline-substitution handlers | +| `LineNumber` | `int` | Zero-based index of the next line to execute; set this to implement jumps | +| `Exit(message, isError)` | `void` | Terminate execution with a message; `isError: true` signals an error |