AddStatement now passes runtime context for custom handlers

This commit is contained in:
Stone_Red
2026-03-06 00:04:47 +01:00
parent 491eedf77e
commit 246e05dcd9
5 changed files with 203 additions and 3 deletions
@@ -4,6 +4,7 @@ using System.Collections.Generic;
using YesNt.Interpreter.Attributes; using YesNt.Interpreter.Attributes;
using YesNt.Interpreter.Enums; using YesNt.Interpreter.Enums;
using YesNt.Interpreter.Runtime;
namespace YesNt.Interpreter.Tests; namespace YesNt.Interpreter.Tests;
@@ -333,4 +334,44 @@ public class AddStatementTests
interpreter.AddStatement("var", SearchMode.StartOfLine, SpaceAround.End, _ => { }); interpreter.AddStatement("var", SearchMode.StartOfLine, SpaceAround.End, _ => { });
}); });
} }
[TestMethod]
public void AddStatementWithRuntimeInfoCanSetVariableTest()
{
List<string> 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<string> 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);
}
} }
@@ -0,0 +1,32 @@
using System.Collections.Generic;
namespace YesNt.Interpreter.Runtime;
/// <summary>
/// Exposes the script runtime state accessible to custom statement handlers registered
/// via <see cref="YesNtInterpreter.AddStatement"/>.
/// </summary>
public interface IStatementContext
{
/// <summary>Gets the local variable table for the current scope.</summary>
Dictionary<string, string> Variables { get; }
/// <summary>Gets or sets the global variable table shared across all scopes.</summary>
Dictionary<string, string> GlobalVariables { get; set; }
/// <summary>Gets or sets the text of the line currently being processed.
/// Inline-substitution handlers (e.g. <c>%read_line</c>) write their result here.</summary>
string CurrentLine { get; set; }
/// <summary>Gets or sets the zero-based index of the next line to execute.
/// Set this to implement control-flow jumps inside a custom statement.</summary>
int LineNumber { get; set; }
/// <summary>Terminates execution with the given message.</summary>
/// <param name="message">The message written to debug output.</param>
/// <param name="isError">
/// <see langword="true"/> to signal an error termination;
/// <see langword="false"/> for a planned, non-error termination.
/// </param>
void Exit(string message, bool isError);
}
@@ -13,7 +13,7 @@ namespace YesNt.Interpreter.Runtime;
/// <see cref="RuntimeInformation"/> whose <see cref="ParentRuntimeInformation"/> points back /// <see cref="RuntimeInformation"/> whose <see cref="ParentRuntimeInformation"/> points back
/// to the main execution context. /// to the main execution context.
/// </summary> /// </summary>
internal sealed class RuntimeInformation internal sealed class RuntimeInformation : IStatementContext
{ {
public event Action<string> OnDebugOutput; public event Action<string> OnDebugOutput;
@@ -111,6 +111,20 @@ public class YesNtInterpreter
PreScanLines(); PreScanLines();
} }
/// <summary>
/// Registers a custom statement using a pre-built <see cref="StatementAttribute"/>,
/// with access to the script's <see cref="IStatementContext"/> (variables, line number, output, etc.).
/// </summary>
/// <param name="attribute">The attribute describing the keyword, search mode, and priority.</param>
/// <param name="handler">
/// The delegate invoked when the statement matches. Receives the argument text and the current
/// <see cref="IStatementContext"/> for reading/writing script state.
/// </param>
public void AddStatement(StatementAttribute attribute, Action<string, IStatementContext> handler)
{
AddStatement(attribute, args => handler(args, runtimeInfo));
}
/// <summary> /// <summary>
/// Registers a simple custom statement with default settings. /// Registers a simple custom statement with default settings.
/// </summary> /// </summary>
@@ -123,6 +137,22 @@ public class YesNtInterpreter
AddStatement(new StatementAttribute(name, searchMode, spaceAround), handler); AddStatement(new StatementAttribute(name, searchMode, spaceAround), handler);
} }
/// <summary>
/// Registers a simple custom statement with default settings,
/// with access to the script's <see cref="IStatementContext"/> (variables, line number, output, etc.).
/// </summary>
/// <param name="name">The keyword to match.</param>
/// <param name="searchMode">Where in the line the keyword is searched for.</param>
/// <param name="spaceAround">Which sides of the keyword must be padded with a space.</param>
/// <param name="handler">
/// The delegate invoked when the statement matches. Receives the argument text and the current
/// <see cref="IStatementContext"/> for reading/writing script state.
/// </param>
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action<string, IStatementContext> handler)
{
AddStatement(new StatementAttribute(name, searchMode, spaceAround), handler);
}
/// <summary> /// <summary>
/// Registers a simple custom statement with a specific syntax-highlight color. /// Registers a simple custom statement with a specific syntax-highlight color.
/// </summary> /// </summary>
@@ -136,6 +166,23 @@ public class YesNtInterpreter
AddStatement(new StatementAttribute(name, searchMode, spaceAround, consoleColor), handler); AddStatement(new StatementAttribute(name, searchMode, spaceAround, consoleColor), handler);
} }
/// <summary>
/// Registers a simple custom statement with a specific syntax-highlight color,
/// with access to the script's <see cref="IStatementContext"/> (variables, line number, output, etc.).
/// </summary>
/// <param name="name">The keyword to match.</param>
/// <param name="searchMode">Where in the line the keyword is searched for.</param>
/// <param name="spaceAround">Which sides of the keyword must be padded with a space.</param>
/// <param name="consoleColor">The color used for syntax highlighting.</param>
/// <param name="handler">
/// The delegate invoked when the statement matches. Receives the argument text and the current
/// <see cref="IStatementContext"/> for reading/writing script state.
/// </param>
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor consoleColor, Action<string, IStatementContext> handler)
{
AddStatement(new StatementAttribute(name, searchMode, spaceAround, consoleColor), handler);
}
/// <summary> /// <summary>
/// Unregisters all handlers matching the specified keyword <paramref name="name"/>. /// Unregisters all handlers matching the specified keyword <paramref name="name"/>.
/// </summary> /// </summary>
+81 -1
View File
@@ -124,6 +124,37 @@ interpreter.AddStatement("log", SearchMode.StartOfLine, SpaceAround.End, args =>
interpreter.Execute(new List<string> { "log Hello from custom statement" }); interpreter.Execute(new List<string> { "log Hello from custom statement" });
``` ```
### Accessing script state from a handler
Pass an `Action<string, IStatementContext>` instead of `Action<string>` 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: <name> <value>", isError: true);
});
```
`IStatementContext` provides:
| Property | Type | Description |
| ------------------------ | --------------------------- | -------------------------------------------------------------------------- |
| `Variables` | `Dictionary<string,string>` | Local variable table for the current scope |
| `GlobalVariables` | `Dictionary<string,string>` | 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 ### With a syntax-highlight colour
```csharp ```csharp
@@ -240,6 +271,9 @@ interpreter.AddStatement("exec", SearchMode.StartOfLine, SpaceAround.End, args =
## Stopping a script ## 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 ```csharp
var interpreter = new YesNtInterpreter(); var interpreter = new YesNtInterpreter();
@@ -253,6 +287,27 @@ interpreter.Stop(); // signals the script to terminate at the next line bounda
thread.Join(); 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<string> { "var key = %read_key" }));
thread.Start();
waitingForInput.WaitOne(TimeSpan.FromSeconds(5)); // wait until blocked on input
interpreter.Stop();
thread.Join();
```
--- ---
## Reading registered statements ## Reading registered statements
@@ -303,10 +358,13 @@ Creates a new interpreter instance and registers all built-in statements.
```csharp ```csharp
public event Action<string> OnDebugOutput; public event Action<string> OnDebugOutput;
public event Action<DebugEventArgs> OnLineExecuted; public event Action<DebugEventArgs> 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. `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 #### Methods
@@ -319,10 +377,13 @@ public void Execute(List<string> lines, bool isDebugMode = false);
// Register a custom statement (full control) // Register a custom statement (full control)
public void AddStatement(StatementAttribute attribute, Action<string> handler); public void AddStatement(StatementAttribute attribute, Action<string> handler);
public void AddStatement(StatementAttribute attribute, Action<string, IStatementContext> handler);
// Register a custom statement (convenience overloads) // Register a custom statement (convenience overloads)
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action<string> handler); public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action<string> handler);
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action<string, IStatementContext> handler);
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor color, Action<string> handler); public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor color, Action<string> handler);
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor color, Action<string, IStatementContext> handler);
// Remove a built-in or custom statement permanently // Remove a built-in or custom statement permanently
public void RemoveStatement(string name); public void RemoveStatement(string name);
@@ -343,3 +404,22 @@ public void Stop();
// Read-only snapshot of all registered statements // Read-only snapshot of all registered statements
public ReadOnlyCollection<StatementInformation> StatementInformation { get; } public ReadOnlyCollection<StatementInformation> StatementInformation { get; }
``` ```
---
### `IStatementContext`
```csharp
public interface IStatementContext // YesNt.Interpreter.Runtime
```
Passed to `Action<string, IStatementContext>` handlers registered via `AddStatement`.
Provides access to the script state that a built-in statement handler would have.
| Member | Type | Description |
| ------------------------ | --------------------------- | ------------------------------------------------------------------------- |
| `Variables` | `Dictionary<string,string>` | Local variable table for the current scope |
| `GlobalVariables` | `Dictionary<string,string>` | 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 |