From 8875f1ff993a84deba2025c47d09fefb282ab117 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:35:20 +0200 Subject: [PATCH 01/10] Replace LINQ ordering with manual sorting for statement entries, replace Attributes form public API with normal data class --- .../StatementRegistryGenerator.cs | 30 ++--- .../AddStatementTests.cs | 9 +- .../Attributes/StatementAttributeContainer.cs | 109 ++++++++++++++++++ .../StaticStatementAttributeContainer.cs | 28 +++++ .../Runtime/StatementHandler.cs | 2 +- .../Runtime/YesNtInterpreter.cs | 59 ++++++---- 6 files changed, 193 insertions(+), 44 deletions(-) create mode 100644 src/YesNt.Interpreter/Attributes/StatementAttributeContainer.cs create mode 100644 src/YesNt.Interpreter/Attributes/StaticStatementAttributeContainer.cs diff --git a/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs b/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs index 242902d..92d6f31 100644 --- a/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs +++ b/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs @@ -87,7 +87,6 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator _ = sb.AppendLine("#nullable enable"); _ = sb.AppendLine("using System;"); _ = sb.AppendLine("using System.Collections.Generic;"); - _ = sb.AppendLine("using System.Linq;"); _ = sb.AppendLine(); _ = sb.AppendLine("namespace YesNt.Interpreter.Runtime;"); _ = sb.AppendLine(); @@ -95,8 +94,8 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator _ = sb.AppendLine("{"); _ = sb.AppendLine(" internal static void Register("); _ = sb.AppendLine(" RuntimeInformation runtimeInfo,"); - _ = sb.AppendLine(" out Dictionary> statements,"); - _ = sb.AppendLine(" out List> staticStatements)"); + _ = sb.AppendLine(" out Dictionary> statements,"); + _ = sb.AppendLine(" out List> staticStatements)"); _ = sb.AppendLine(" {"); List allTypes = statementMethods @@ -117,36 +116,39 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator _ = sb.AppendLine($" {instanceName}.RuntimeInfo = runtimeInfo;"); } - _ = sb.AppendLine(" var statementEntries = new List>>();"); + _ = sb.AppendLine(" var statementEntries = new List>>();"); foreach (MethodRegistration method in statementMethods .OrderBy(x => x.ContainingType.ToDisplayString()) .ThenBy(x => x.Method.Name)) { string instanceName = instanceNames[method.ContainingType]; - string attributeCreation = BuildAttributeCreation("global::YesNt.Interpreter.Attributes.StatementAttribute", method.Attribute); + string attributeCreation = BuildAttributeCreation("global::YesNt.Interpreter.Attributes.StatementAttributeContainer", method.Attribute); _ = sb.AppendLine($" statementEntries.Add(new({attributeCreation}, {instanceName}.{method.Method.Name}));"); } - _ = sb.AppendLine(" var staticEntries = new List>();"); + _ = sb.AppendLine(" var staticEntries = new List>();"); foreach (MethodRegistration method in staticStatementMethods .OrderBy(x => x.ContainingType.ToDisplayString()) .ThenBy(x => x.Method.Name)) { string instanceName = instanceNames[method.ContainingType]; - string attributeCreation = BuildAttributeCreation("global::YesNt.Interpreter.Attributes.StaticStatementAttribute", method.Attribute); + string attributeCreation = BuildAttributeCreation("global::YesNt.Interpreter.Attributes.StaticStatementAttributeContainer", method.Attribute); _ = sb.AppendLine($" staticEntries.Add(new({attributeCreation}, {instanceName}.{method.Method.Name}));"); } - _ = sb.AppendLine(" statements = statementEntries"); - _ = sb.AppendLine(" .OrderBy(s => s.Key.Priority)"); - _ = sb.AppendLine(" .ThenByDescending(s => s.Key.Name.Length)"); - _ = sb.AppendLine(" .ToDictionary(x => x.Key, x => x.Value);"); + _ = sb.AppendLine(" statementEntries.Sort((a, b) =>"); + _ = sb.AppendLine(" {"); + _ = sb.AppendLine(" int cmp = a.Key.Priority.CompareTo(b.Key.Priority);"); + _ = sb.AppendLine(" return cmp != 0 ? cmp : b.Key.Name.Length.CompareTo(a.Key.Name.Length);"); + _ = sb.AppendLine(" });"); + _ = sb.AppendLine(" statements = new Dictionary>();"); + _ = sb.AppendLine(" foreach (var entry in statementEntries)"); + _ = sb.AppendLine(" statements.Add(entry.Key, entry.Value);"); _ = sb.AppendLine(); - _ = sb.AppendLine(" staticStatements = staticEntries"); - _ = sb.AppendLine(" .OrderBy(s => s.Key.Priority)"); - _ = sb.AppendLine(" .ToList();"); + _ = sb.AppendLine(" staticEntries.Sort((a, b) => a.Key.Priority.CompareTo(b.Key.Priority));"); + _ = sb.AppendLine(" staticStatements = staticEntries;"); _ = sb.AppendLine(" }"); _ = sb.AppendLine("}"); diff --git a/src/YesNt.Interpreter.Tests/AddStatementTests.cs b/src/YesNt.Interpreter.Tests/AddStatementTests.cs index f604839..73d8c14 100644 --- a/src/YesNt.Interpreter.Tests/AddStatementTests.cs +++ b/src/YesNt.Interpreter.Tests/AddStatementTests.cs @@ -4,7 +4,6 @@ using System.Collections.Generic; using YesNt.Interpreter.Attributes; using YesNt.Interpreter.Enums; -using YesNt.Interpreter.Runtime; namespace YesNt.Interpreter.Tests; @@ -65,7 +64,7 @@ public class AddStatementTests _ = YesNtAssert.GetLastLineWithSetup(lines, interpreter => { - StatementAttribute attr = new StatementAttribute("attr_cmd", SearchMode.StartOfLine, SpaceAround.End); + StatementAttributeContainer attr = new StatementAttributeContainer("attr_cmd", SearchMode.StartOfLine, SpaceAround.End); interpreter.AddStatement(attr, _ => { handlerCalled = true; @@ -282,11 +281,11 @@ public class AddStatementTests _ = YesNtAssert.GetLastLineWithSetup(lines, interpreter => { interpreter.AddStatement( - new StatementAttribute("priority_cmd", SearchMode.StartOfLine, SpaceAround.End) { Priority = Priority.High }, + new StatementAttributeContainer("priority_cmd", SearchMode.StartOfLine, SpaceAround.End) { Priority = Priority.High }, _ => highPriorityOrder = callOrder++); interpreter.AddStatement( - new StatementAttribute("priority_cmd", SearchMode.StartOfLine, SpaceAround.End) { Priority = Priority.Normal }, + new StatementAttributeContainer("priority_cmd", SearchMode.StartOfLine, SpaceAround.End) { Priority = Priority.Normal }, _ => normalPriorityOrder = callOrder++); }); @@ -364,7 +363,7 @@ public class AddStatementTests string? captured = null; - YesNtAssert.GetLastLineWithSetup(lines, interpreter => + _ = YesNtAssert.GetLastLineWithSetup(lines, interpreter => { interpreter.AddStatement("echo_var", SearchMode.StartOfLine, SpaceAround.End, (args, rt) => { diff --git a/src/YesNt.Interpreter/Attributes/StatementAttributeContainer.cs b/src/YesNt.Interpreter/Attributes/StatementAttributeContainer.cs new file mode 100644 index 0000000..275bfb5 --- /dev/null +++ b/src/YesNt.Interpreter/Attributes/StatementAttributeContainer.cs @@ -0,0 +1,109 @@ +using System; + +using YesNt.Interpreter.Enums; + +namespace YesNt.Interpreter.Attributes; + +/// +/// Marks a method as a YesNt statement handler. +/// The interpreter matches source lines against the keyword according to +/// and rules, then invokes the decorated method +/// with the remaining argument text. +/// +/// +/// Methods decorated with this attribute must be instance methods on a class that inherits +/// and must accept a single parameter. +/// +public class StatementAttributeContainer +{ + /// Gets the keyword that identifies this statement in source code. + public string Name { get; } + + /// Gets where in the line the keyword is searched for. + public SearchMode SearchMode { get; } + + /// Gets which sides of the keyword must be padded with a space. + public SpaceAround SpaceAround { get; } + + /// Gets or sets the syntax-highlight color used by the code editor. + public ConsoleColor Color { get; set; } + + /// + /// Gets or sets the execution priority. Statements with a lower value + /// run before those with a higher value. Defaults to . + /// + public Priority Priority { get; set; } = Priority.Normal; + + /// + /// Gets or sets a value indicating whether this statement is still invoked while the interpreter + /// is in search mode (scanning for a label or function definition). Defaults to . + /// + public bool ExecuteInSearchMode { get; set; } + + /// + /// Gets or sets a value indicating whether the full current line (including the keyword itself) + /// is passed as the argument, rather than stripping the keyword prefix/suffix first. + /// Defaults to . + /// + public bool KeepStatementInArgs { get; set; } + + /// + /// Gets a value indicating whether this statement should be excluded from syntax highlighting. + /// Set to when no is provided. + /// + public bool IgnoreSyntaxHighlighting { get; } + + /// + /// Gets or sets an optional sub-string that must also be present in the line for this statement + /// to match. Used to differentiate overloaded keywords (e.g. call vs call … with …). + /// + public string Separator { get; set; } + + /// + /// Gets or sets the name of the statement that marks the end of this block. + /// Used for block boundary caching (e.g., "while" has BlockPair = "end_while"). + /// + public string BlockPair { get; set; } + + /// + /// Gets or sets a value indicating whether this statement is the end of a block. + /// Used for block boundary caching (e.g., "end_while" has IsBlockEnd = true). + /// + public bool IsBlockEnd { get; set; } + + /// + /// Gets or sets a value indicating whether this statement is an intermediate part of a block + /// (e.g., "else:" between "if" and "end_if"). + /// + public bool IsBlockIntermediate { get; set; } + + /// + /// Initializes a new with a syntax-highlight color. + /// + /// The keyword that identifies this statement. + /// Where in the line the keyword is matched. + /// Which sides of the keyword require a surrounding space. + /// The color used for syntax highlighting in the code editor. + public StatementAttributeContainer(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor color) + { + Name = name; + SearchMode = searchMode; + SpaceAround = spaceAround; + Color = color; + } + + /// + /// Initializes a new without a syntax-highlight color. + /// The statement will be excluded from syntax highlighting. + /// + /// The keyword that identifies this statement. + /// Where in the line the keyword is matched. + /// Which sides of the keyword require a surrounding space. + public StatementAttributeContainer(string name, SearchMode searchMode, SpaceAround spaceAround) + { + Name = name; + SearchMode = searchMode; + SpaceAround = spaceAround; + IgnoreSyntaxHighlighting = true; + } +} \ No newline at end of file diff --git a/src/YesNt.Interpreter/Attributes/StaticStatementAttributeContainer.cs b/src/YesNt.Interpreter/Attributes/StaticStatementAttributeContainer.cs new file mode 100644 index 0000000..3d8afa4 --- /dev/null +++ b/src/YesNt.Interpreter/Attributes/StaticStatementAttributeContainer.cs @@ -0,0 +1,28 @@ +using YesNt.Interpreter.Enums; + +namespace YesNt.Interpreter.Attributes; + +/// +/// Marks a parameterless method as a YesNt static statement handler. +/// Static statements are invoked once per line before regular statement matching begins, +/// regardless of whether the line matches any keyword. They are typically used for +/// pre-processing tasks such as transforming the current line before other statements run. +/// +/// +/// Methods decorated with this attribute must be instance methods on a class that inherits +/// and must have no parameters. +/// +public class StaticStatementAttributeContainer +{ + /// + /// Gets or sets a value indicating whether this handler is still invoked while the interpreter + /// is in search mode (scanning for a label or function definition). Defaults to . + /// + public bool ExecuteInSearchMode { get; set; } + + /// + /// Gets or sets the execution priority relative to other static statements. + /// Defaults to . + /// + public Priority Priority { get; set; } = Priority.Normal; +} \ No newline at end of file diff --git a/src/YesNt.Interpreter/Runtime/StatementHandler.cs b/src/YesNt.Interpreter/Runtime/StatementHandler.cs index 1a2d66c..b45d590 100644 --- a/src/YesNt.Interpreter/Runtime/StatementHandler.cs +++ b/src/YesNt.Interpreter/Runtime/StatementHandler.cs @@ -7,4 +7,4 @@ namespace YesNt.Interpreter.Runtime; /// /// Pre-calculated statement handler information for faster matching. /// -internal record StatementHandler(StatementAttribute Attribute, Action Handler, string FullName); \ No newline at end of file +internal record StatementHandler(StatementAttributeContainer Attribute, Action Handler, string FullName); \ No newline at end of file diff --git a/src/YesNt.Interpreter/Runtime/YesNtInterpreter.cs b/src/YesNt.Interpreter/Runtime/YesNtInterpreter.cs index 2707aad..c7f3752 100644 --- a/src/YesNt.Interpreter/Runtime/YesNtInterpreter.cs +++ b/src/YesNt.Interpreter/Runtime/YesNtInterpreter.cs @@ -28,11 +28,11 @@ public class YesNtInterpreter public event Action OnDebugOutput; private readonly RuntimeInformation runtimeInfo = new RuntimeInformation(); - private Dictionary> statements; + private Dictionary> statements; private List statementHandlers; private List> lineMatchingHandlers = []; - private readonly List> staticStatements; - private readonly Dictionary>>> disabledStatements = []; + private readonly List> staticStatements; + private readonly Dictionary>>> disabledStatements = []; /// /// Gets a read-only snapshot of all currently registered statements. @@ -88,7 +88,7 @@ public class YesNtInterpreter } /// - /// Registers a custom statement using a pre-built . + /// Registers a custom statement using a pre-built . /// If a statement with the same attribute key (identical field values) already exists it will be replaced; /// otherwise a new entry is added. Built-in statements use distinct attribute instances, so passing a /// newly constructed attribute with the same name will add a second handler rather than replacing @@ -98,15 +98,26 @@ public class YesNtInterpreter /// The attribute describing the keyword, search mode, and priority. /// /// The delegate invoked when the statement matches. Receives the argument text - /// (the part of the line after the keyword, unless is set). + /// (the part of the line after the keyword, unless is set). /// - public void AddStatement(StatementAttribute attribute, Action handler) + public void AddStatement(StatementAttributeContainer attribute, Action handler) { statements[attribute] = handler; - statements = statements - .OrderBy(s => s.Key.Priority) - .ThenByDescending(s => s.Key.Name.Length) - .ToDictionary(x => x.Key, x => x.Value); + + List>> entries = [.. statements]; + entries.Sort((a, b) => + { + int cmp = a.Key.Priority.CompareTo(b.Key.Priority); + return cmp != 0 ? cmp : b.Key.Name.Length.CompareTo(a.Key.Name.Length); + }); + + statements = []; + + foreach (KeyValuePair> entry in entries) + { + statements.Add(entry.Key, entry.Value); + } + UpdateStatementHandlers(); PreScanLines(); } @@ -120,7 +131,7 @@ public class YesNtInterpreter /// 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) + public void AddStatement(StatementAttributeContainer attribute, Action handler) { AddStatement(attribute, args => handler(args, runtimeInfo)); } @@ -134,7 +145,7 @@ public class YesNtInterpreter /// The delegate invoked when the statement matches. public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action handler) { - AddStatement(new StatementAttribute(name, searchMode, spaceAround), handler); + AddStatement(new StatementAttributeContainer(name, searchMode, spaceAround), handler); } /// @@ -150,7 +161,7 @@ public class YesNtInterpreter /// public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action handler) { - AddStatement(new StatementAttribute(name, searchMode, spaceAround), handler); + AddStatement(new StatementAttributeContainer(name, searchMode, spaceAround), handler); } /// @@ -163,7 +174,7 @@ public class YesNtInterpreter /// The delegate invoked when the statement matches. public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor consoleColor, Action handler) { - AddStatement(new StatementAttribute(name, searchMode, spaceAround, consoleColor), handler); + AddStatement(new StatementAttributeContainer(name, searchMode, spaceAround, consoleColor), handler); } /// @@ -180,7 +191,7 @@ public class YesNtInterpreter /// public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor consoleColor, Action handler) { - AddStatement(new StatementAttribute(name, searchMode, spaceAround, consoleColor), handler); + AddStatement(new StatementAttributeContainer(name, searchMode, spaceAround, consoleColor), handler); } /// @@ -189,7 +200,7 @@ public class YesNtInterpreter /// The keyword to remove. public void RemoveStatement(string name) { - foreach (StatementAttribute key in statements.Keys.Where(k => k.Name == name).ToList()) + foreach (StatementAttributeContainer key in statements.Keys.Where(k => k.Name == name).ToList()) { _ = statements.Remove(key); } @@ -212,7 +223,7 @@ public class YesNtInterpreter return; } - List>> matching = + List>> matching = statements.Where(kv => kv.Key.Name == name).ToList(); if (matching.Count == 0) @@ -222,7 +233,7 @@ public class YesNtInterpreter disabledStatements[name] = matching; - foreach (KeyValuePair> kv in matching) + foreach (KeyValuePair> kv in matching) { statements[kv.Key] = _ => { }; } @@ -238,12 +249,12 @@ public class YesNtInterpreter /// The keyword of the statement(s) to re-enable. public void EnableStatement(string name) { - if (!disabledStatements.TryGetValue(name, out List>> saved)) + if (!disabledStatements.TryGetValue(name, out List>> saved)) { return; } - foreach (KeyValuePair> kv in saved) + foreach (KeyValuePair> kv in saved) { statements[kv.Key] = kv.Value; } @@ -349,9 +360,9 @@ public class YesNtInterpreter }; } - foreach (KeyValuePair staticStatement in staticStatements) + foreach (KeyValuePair staticStatement in staticStatements) { - StaticStatementAttribute staticStatementAttribute = staticStatement.Key; + StaticStatementAttributeContainer staticStatementAttribute = staticStatement.Key; if (!staticStatementAttribute.ExecuteInSearchMode && runtimeInfo.IsSearching) { continue; @@ -367,7 +378,7 @@ public class YesNtInterpreter foreach (StatementHandler handler in handlers) { - StatementAttribute statementAttribute = handler.Attribute; + StatementAttributeContainer statementAttribute = handler.Attribute; if (!statementAttribute.ExecuteInSearchMode && runtimeInfo.IsSearching) { @@ -534,7 +545,7 @@ public class YesNtInterpreter private static bool IsPossibleMatch(string content, StatementHandler handler) { - StatementAttribute attr = handler.Attribute; + StatementAttributeContainer attr = handler.Attribute; string fullName = handler.FullName; return attr.SearchMode switch From a465897f3cfe93e044ee1574cbe1ae914d1f9e48 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:35:38 +0200 Subject: [PATCH 02/10] Fix remove extra spaces from %read_line and %read_key replacements --- src/YesNt.Interpreter/Statements/ConsoleStatements.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/YesNt.Interpreter/Statements/ConsoleStatements.cs b/src/YesNt.Interpreter/Statements/ConsoleStatements.cs index 0c391d7..bc9ec62 100644 --- a/src/YesNt.Interpreter/Statements/ConsoleStatements.cs +++ b/src/YesNt.Interpreter/Statements/ConsoleStatements.cs @@ -41,7 +41,7 @@ internal class ConsoleStatements : StatementRuntimeInformation RuntimeInfo.Exit(ExitMessages.TerminatedByExternalProcess, true); return; } - args = args.ReplaceFirstOccurrence("%read_line ", input.ToSafeString() + " "); + args = args.ReplaceFirstOccurrence("%read_line", input.ToSafeString()); } RuntimeInfo.CurrentLine = args.TrimEnd(); } @@ -53,7 +53,7 @@ internal class ConsoleStatements : StatementRuntimeInformation while (args.Contains("%read_key")) { string input = ConsoleExtensions.ReadKey(RuntimeInfo).ToString(); - args = args.ReplaceFirstOccurrence("%read_key ", input.ToSafeString() + " "); + args = args.ReplaceFirstOccurrence("%read_key", input.ToSafeString()); } RuntimeInfo.CurrentLine = args.TrimEnd(); } From 5ad0053ef7a14d6be0b83d4e8bc1757099b03898 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Thu, 11 Jun 2026 01:02:24 +0200 Subject: [PATCH 03/10] Add stepwise and time-based script execution with Step and RunFor methods --- src/YesNt.Interpreter/Runtime/StepResult.cs | 27 +++ .../Runtime/YesNtInterpreter.cs | 200 +++++++++++++++--- 2 files changed, 195 insertions(+), 32 deletions(-) create mode 100644 src/YesNt.Interpreter/Runtime/StepResult.cs 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) From 1760b85245ad1f7a87abc2097091c5d382de1ac8 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:47:59 +0200 Subject: [PATCH 04/10] Generate StatementInformation classes with code generator --- .../StatementRegistryGenerator.cs | 207 +++++++++++++++++- .../AddStatementTests.cs | 8 +- .../Attributes/StatementAttributeContainer.cs | 109 --------- .../StaticStatementAttributeContainer.cs | 28 --- .../Runtime/IStatementContext.cs | 10 +- .../Runtime/StatementHandler.cs | 4 +- .../Runtime/StatementInformation.cs | 35 --- .../Runtime/YesNtInterpreter.cs | 63 ++---- 8 files changed, 229 insertions(+), 235 deletions(-) delete mode 100644 src/YesNt.Interpreter/Attributes/StatementAttributeContainer.cs delete mode 100644 src/YesNt.Interpreter/Attributes/StaticStatementAttributeContainer.cs delete mode 100644 src/YesNt.Interpreter/Runtime/StatementInformation.cs diff --git a/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs b/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs index 92d6f31..ee28b58 100644 --- a/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs +++ b/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs @@ -27,8 +27,14 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator CollectMethods(compilation.Assembly.GlobalNamespace, statementMethods, staticStatementMethods); - string source = GenerateRegistrySource(statementMethods, staticStatementMethods); - context.AddSource("GeneratedStatementRegistry.g.cs", source); + INamedTypeSymbol? statementAttrSymbol = compilation.GetTypeByMetadataName(StatementAttributeName); + INamedTypeSymbol? staticStatementAttrSymbol = compilation.GetTypeByMetadataName(StaticStatementAttributeName); + + string infoSource = GenerateInformationClassesSource(statementAttrSymbol, staticStatementAttrSymbol); + context.AddSource("GeneratedStatementInformation.g.cs", infoSource); + + string registrySource = GenerateRegistrySource(statementMethods, staticStatementMethods); + context.AddSource("GeneratedStatementRegistry.g.cs", registrySource); } private static void CollectMethods( @@ -77,6 +83,187 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator } } + // ------------------------------------------------------------------------- + // Information class generation + // ------------------------------------------------------------------------- + + private static string GenerateInformationClassesSource( + INamedTypeSymbol? statementAttrSymbol, + INamedTypeSymbol? staticStatementAttrSymbol) + { + StringBuilder sb = new StringBuilder(); + + _ = sb.AppendLine("// "); + _ = sb.AppendLine("#nullable enable"); + _ = sb.AppendLine(); + _ = sb.AppendLine("namespace YesNt.Interpreter.Runtime;"); + _ = sb.AppendLine(); + + if (statementAttrSymbol is not null) + { + EmitInformationClass(sb, statementAttrSymbol, "StatementInformation"); + } + + if (staticStatementAttrSymbol is not null) + { + EmitInformationClass(sb, staticStatementAttrSymbol, "StaticStatementInformation"); + } + + return sb.ToString(); + } + + private static void EmitInformationClass( + StringBuilder sb, + INamedTypeSymbol attributeSymbol, + string className) + { + List ctors = attributeSymbol.Constructors + .Where(c => !c.IsImplicitlyDeclared) + .OrderByDescending(c => c.Parameters.Length) + .ToList(); + + // All parameters across all ctors that are covered by a ctor (get-only properties). + HashSet ctorParamNames = new HashSet( + ctors.SelectMany(c => c.Parameters).Select(p => p.Name), + StringComparer.OrdinalIgnoreCase); + + // Settable properties NOT covered by any ctor parameter. + List settableProps = attributeSymbol + .GetMembers() + .OfType() + .Where(p => !p.IsStatic + && p.DeclaredAccessibility == Accessibility.Public + && p.SetMethod is not null + && !ctorParamNames.Contains(p.Name)) + .ToList(); + + // All properties ever assigned by any ctor, deduplicated by name. + // We need a get-only property for each of them. + List<(string PropName, ITypeSymbol PropType)> ctorProps = ctors + .SelectMany(c => c.Parameters) + .Select(p => ( + PropName: ResolvePropertyName(attributeSymbol, p), + PropType: p.Type)) + .GroupBy(x => x.PropName, StringComparer.OrdinalIgnoreCase) + .Select(g => g.First()) + .ToList(); + + // ---- class declaration ---- + _ = sb.AppendLine($"public sealed class {className}"); + _ = sb.AppendLine("{"); + + // ---- one constructor per attribute constructor ---- + foreach (IMethodSymbol ctor in ctors) + { + if (ctor.Parameters.Length == 0) + { + _ = sb.AppendLine($" public {className}() {{ }}"); + _ = sb.AppendLine(); + continue; + } + + string ctorParams = string.Join(", ", + ctor.Parameters.Select(p => $"{GlobalType(p.Type)} {p.Name}")); + + _ = sb.AppendLine($" public {className}({ctorParams})"); + _ = sb.AppendLine(" {"); + + foreach (IParameterSymbol p in ctor.Parameters) + { + string propName = ResolvePropertyName(attributeSymbol, p); + _ = sb.AppendLine($" {propName} = {p.Name};"); + } + + _ = sb.AppendLine(" }"); + _ = sb.AppendLine(); + } + + // ---- get-only properties sourced from ctor parameters ---- + foreach ((string propName, ITypeSymbol propType) in ctorProps) + { + _ = sb.AppendLine($" public {GlobalType(propType)} {propName} {{ get; }}"); + } + + // ---- settable properties (named-argument style) ---- + foreach (IPropertySymbol prop in settableProps) + { + string defaultClause = GetDefaultClause(prop); + _ = sb.AppendLine($" public {GlobalType(prop.Type)} {prop.Name} {{ get; init; }}{defaultClause}"); + } + + _ = sb.AppendLine("}"); + _ = sb.AppendLine(); + } + + // Finds the attribute property that corresponds to a constructor parameter. + private static string ResolvePropertyName(INamedTypeSymbol attributeSymbol, IParameterSymbol parameter) + { + IPropertySymbol? match = attributeSymbol + .GetMembers() + .OfType() + .FirstOrDefault(p => string.Equals(p.Name, parameter.Name, StringComparison.OrdinalIgnoreCase)); + + return match?.Name ?? ToPascalCase(parameter.Name); + } + + private static string GlobalType(ITypeSymbol type) + { + // Primitive aliases don't have a global:: form — emit the C# keyword instead. + string keyword = type.SpecialType switch + { + SpecialType.System_String => "string", + SpecialType.System_Boolean => "bool", + SpecialType.System_Byte => "byte", + SpecialType.System_SByte => "sbyte", + SpecialType.System_Int16 => "short", + SpecialType.System_UInt16 => "ushort", + SpecialType.System_Int32 => "int", + SpecialType.System_UInt32 => "uint", + SpecialType.System_Int64 => "long", + SpecialType.System_UInt64 => "ulong", + SpecialType.System_Single => "float", + SpecialType.System_Double => "double", + SpecialType.System_Decimal => "decimal", + SpecialType.System_Char => "char", + SpecialType.System_Object => "object", + _ => "" + }; + + if (keyword != "") + { + // Preserve nullability annotation (e.g. string?) + return type.NullableAnnotation == NullableAnnotation.Annotated + ? keyword + "?" + : keyword; + } + + return $"global::{type.ToDisplayString()}"; + } + + private static string ToPascalCase(string name) + { + return string.IsNullOrEmpty(name) ? name : char.ToUpperInvariant(name[0]) + name.Substring(1); + } + + private static string GetDefaultClause(IPropertySymbol prop) + { + // Only emit a default for a handful of well-known "safe" defaults so the + // generated code compiles even when the caller omits the named argument. + return prop.Type.IsReferenceType || prop.Type.NullableAnnotation == NullableAnnotation.Annotated + ? " = null!;" + : prop.Type.SpecialType switch + { + SpecialType.System_Boolean => " = false;", + SpecialType.System_Int32 => " = 0;", + SpecialType.System_String => " = \"\";", + _ => "" + }; + } + + // ------------------------------------------------------------------------- + // Registry source generation (unchanged from original) + // ------------------------------------------------------------------------- + private static string GenerateRegistrySource( List statementMethods, List staticStatementMethods) @@ -94,8 +281,8 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator _ = sb.AppendLine("{"); _ = sb.AppendLine(" internal static void Register("); _ = sb.AppendLine(" RuntimeInformation runtimeInfo,"); - _ = sb.AppendLine(" out Dictionary> statements,"); - _ = sb.AppendLine(" out List> staticStatements)"); + _ = sb.AppendLine(" out Dictionary> statements,"); + _ = sb.AppendLine(" out List> staticStatements)"); _ = sb.AppendLine(" {"); List allTypes = statementMethods @@ -116,25 +303,25 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator _ = sb.AppendLine($" {instanceName}.RuntimeInfo = runtimeInfo;"); } - _ = sb.AppendLine(" var statementEntries = new List>>();"); + _ = sb.AppendLine(" var statementEntries = new List>>();"); foreach (MethodRegistration method in statementMethods .OrderBy(x => x.ContainingType.ToDisplayString()) .ThenBy(x => x.Method.Name)) { string instanceName = instanceNames[method.ContainingType]; - string attributeCreation = BuildAttributeCreation("global::YesNt.Interpreter.Attributes.StatementAttributeContainer", method.Attribute); + string attributeCreation = BuildAttributeCreation("global::YesNt.Interpreter.Runtime.StatementInformation", method.Attribute); _ = sb.AppendLine($" statementEntries.Add(new({attributeCreation}, {instanceName}.{method.Method.Name}));"); } - _ = sb.AppendLine(" var staticEntries = new List>();"); + _ = sb.AppendLine(" var staticEntries = new List>();"); foreach (MethodRegistration method in staticStatementMethods .OrderBy(x => x.ContainingType.ToDisplayString()) .ThenBy(x => x.Method.Name)) { string instanceName = instanceNames[method.ContainingType]; - string attributeCreation = BuildAttributeCreation("global::YesNt.Interpreter.Attributes.StaticStatementAttributeContainer", method.Attribute); + string attributeCreation = BuildAttributeCreation("global::YesNt.Interpreter.Runtime.StaticStatementInformation", method.Attribute); _ = sb.AppendLine($" staticEntries.Add(new({attributeCreation}, {instanceName}.{method.Method.Name}));"); } @@ -143,7 +330,7 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator _ = sb.AppendLine(" int cmp = a.Key.Priority.CompareTo(b.Key.Priority);"); _ = sb.AppendLine(" return cmp != 0 ? cmp : b.Key.Name.Length.CompareTo(a.Key.Name.Length);"); _ = sb.AppendLine(" });"); - _ = sb.AppendLine(" statements = new Dictionary>();"); + _ = sb.AppendLine(" statements = new Dictionary>();"); _ = sb.AppendLine(" foreach (var entry in statementEntries)"); _ = sb.AppendLine(" statements.Add(entry.Key, entry.Value);"); _ = sb.AppendLine(); @@ -236,4 +423,4 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator public AttributeData Attribute { get; } = attribute; } -} +} \ No newline at end of file diff --git a/src/YesNt.Interpreter.Tests/AddStatementTests.cs b/src/YesNt.Interpreter.Tests/AddStatementTests.cs index 73d8c14..1aeb9d2 100644 --- a/src/YesNt.Interpreter.Tests/AddStatementTests.cs +++ b/src/YesNt.Interpreter.Tests/AddStatementTests.cs @@ -2,8 +2,8 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Collections.Generic; -using YesNt.Interpreter.Attributes; using YesNt.Interpreter.Enums; +using YesNt.Interpreter.Runtime; namespace YesNt.Interpreter.Tests; @@ -64,7 +64,7 @@ public class AddStatementTests _ = YesNtAssert.GetLastLineWithSetup(lines, interpreter => { - StatementAttributeContainer attr = new StatementAttributeContainer("attr_cmd", SearchMode.StartOfLine, SpaceAround.End); + StatementInformation attr = new StatementInformation("attr_cmd", SearchMode.StartOfLine, SpaceAround.End); interpreter.AddStatement(attr, _ => { handlerCalled = true; @@ -281,11 +281,11 @@ public class AddStatementTests _ = YesNtAssert.GetLastLineWithSetup(lines, interpreter => { interpreter.AddStatement( - new StatementAttributeContainer("priority_cmd", SearchMode.StartOfLine, SpaceAround.End) { Priority = Priority.High }, + new StatementInformation("priority_cmd", SearchMode.StartOfLine, SpaceAround.End) { Priority = Priority.High }, _ => highPriorityOrder = callOrder++); interpreter.AddStatement( - new StatementAttributeContainer("priority_cmd", SearchMode.StartOfLine, SpaceAround.End) { Priority = Priority.Normal }, + new StatementInformation("priority_cmd", SearchMode.StartOfLine, SpaceAround.End) { Priority = Priority.Normal }, _ => normalPriorityOrder = callOrder++); }); diff --git a/src/YesNt.Interpreter/Attributes/StatementAttributeContainer.cs b/src/YesNt.Interpreter/Attributes/StatementAttributeContainer.cs deleted file mode 100644 index 275bfb5..0000000 --- a/src/YesNt.Interpreter/Attributes/StatementAttributeContainer.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; - -using YesNt.Interpreter.Enums; - -namespace YesNt.Interpreter.Attributes; - -/// -/// Marks a method as a YesNt statement handler. -/// The interpreter matches source lines against the keyword according to -/// and rules, then invokes the decorated method -/// with the remaining argument text. -/// -/// -/// Methods decorated with this attribute must be instance methods on a class that inherits -/// and must accept a single parameter. -/// -public class StatementAttributeContainer -{ - /// Gets the keyword that identifies this statement in source code. - public string Name { get; } - - /// Gets where in the line the keyword is searched for. - public SearchMode SearchMode { get; } - - /// Gets which sides of the keyword must be padded with a space. - public SpaceAround SpaceAround { get; } - - /// Gets or sets the syntax-highlight color used by the code editor. - public ConsoleColor Color { get; set; } - - /// - /// Gets or sets the execution priority. Statements with a lower value - /// run before those with a higher value. Defaults to . - /// - public Priority Priority { get; set; } = Priority.Normal; - - /// - /// Gets or sets a value indicating whether this statement is still invoked while the interpreter - /// is in search mode (scanning for a label or function definition). Defaults to . - /// - public bool ExecuteInSearchMode { get; set; } - - /// - /// Gets or sets a value indicating whether the full current line (including the keyword itself) - /// is passed as the argument, rather than stripping the keyword prefix/suffix first. - /// Defaults to . - /// - public bool KeepStatementInArgs { get; set; } - - /// - /// Gets a value indicating whether this statement should be excluded from syntax highlighting. - /// Set to when no is provided. - /// - public bool IgnoreSyntaxHighlighting { get; } - - /// - /// Gets or sets an optional sub-string that must also be present in the line for this statement - /// to match. Used to differentiate overloaded keywords (e.g. call vs call … with …). - /// - public string Separator { get; set; } - - /// - /// Gets or sets the name of the statement that marks the end of this block. - /// Used for block boundary caching (e.g., "while" has BlockPair = "end_while"). - /// - public string BlockPair { get; set; } - - /// - /// Gets or sets a value indicating whether this statement is the end of a block. - /// Used for block boundary caching (e.g., "end_while" has IsBlockEnd = true). - /// - public bool IsBlockEnd { get; set; } - - /// - /// Gets or sets a value indicating whether this statement is an intermediate part of a block - /// (e.g., "else:" between "if" and "end_if"). - /// - public bool IsBlockIntermediate { get; set; } - - /// - /// Initializes a new with a syntax-highlight color. - /// - /// The keyword that identifies this statement. - /// Where in the line the keyword is matched. - /// Which sides of the keyword require a surrounding space. - /// The color used for syntax highlighting in the code editor. - public StatementAttributeContainer(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor color) - { - Name = name; - SearchMode = searchMode; - SpaceAround = spaceAround; - Color = color; - } - - /// - /// Initializes a new without a syntax-highlight color. - /// The statement will be excluded from syntax highlighting. - /// - /// The keyword that identifies this statement. - /// Where in the line the keyword is matched. - /// Which sides of the keyword require a surrounding space. - public StatementAttributeContainer(string name, SearchMode searchMode, SpaceAround spaceAround) - { - Name = name; - SearchMode = searchMode; - SpaceAround = spaceAround; - IgnoreSyntaxHighlighting = true; - } -} \ No newline at end of file diff --git a/src/YesNt.Interpreter/Attributes/StaticStatementAttributeContainer.cs b/src/YesNt.Interpreter/Attributes/StaticStatementAttributeContainer.cs deleted file mode 100644 index 3d8afa4..0000000 --- a/src/YesNt.Interpreter/Attributes/StaticStatementAttributeContainer.cs +++ /dev/null @@ -1,28 +0,0 @@ -using YesNt.Interpreter.Enums; - -namespace YesNt.Interpreter.Attributes; - -/// -/// Marks a parameterless method as a YesNt static statement handler. -/// Static statements are invoked once per line before regular statement matching begins, -/// regardless of whether the line matches any keyword. They are typically used for -/// pre-processing tasks such as transforming the current line before other statements run. -/// -/// -/// Methods decorated with this attribute must be instance methods on a class that inherits -/// and must have no parameters. -/// -public class StaticStatementAttributeContainer -{ - /// - /// Gets or sets a value indicating whether this handler is still invoked while the interpreter - /// is in search mode (scanning for a label or function definition). Defaults to . - /// - public bool ExecuteInSearchMode { get; set; } - - /// - /// Gets or sets the execution priority relative to other static statements. - /// Defaults to . - /// - public Priority Priority { get; set; } = Priority.Normal; -} \ No newline at end of file diff --git a/src/YesNt.Interpreter/Runtime/IStatementContext.cs b/src/YesNt.Interpreter/Runtime/IStatementContext.cs index 58569e8..7fc9f8d 100644 --- a/src/YesNt.Interpreter/Runtime/IStatementContext.cs +++ b/src/YesNt.Interpreter/Runtime/IStatementContext.cs @@ -4,7 +4,7 @@ namespace YesNt.Interpreter.Runtime; /// /// Exposes the script runtime state accessible to custom statement handlers registered -/// via . +/// via . /// public interface IStatementContext { @@ -24,9 +24,9 @@ public interface IStatementContext /// Terminates execution with the given message. /// The message written to debug output. - /// - /// to signal an error termination; - /// for a planned, non-error termination. + /// + /// If , also terminates all tasks spawned by the task statement. + /// If , only terminates the current execution context (main script or individual task). /// - void Exit(string message, bool isError); + void Exit(string message, bool stopAllTasks); } diff --git a/src/YesNt.Interpreter/Runtime/StatementHandler.cs b/src/YesNt.Interpreter/Runtime/StatementHandler.cs index b45d590..e98b37c 100644 --- a/src/YesNt.Interpreter/Runtime/StatementHandler.cs +++ b/src/YesNt.Interpreter/Runtime/StatementHandler.cs @@ -1,10 +1,8 @@ using System; -using YesNt.Interpreter.Attributes; - namespace YesNt.Interpreter.Runtime; /// /// Pre-calculated statement handler information for faster matching. /// -internal record StatementHandler(StatementAttributeContainer Attribute, Action Handler, string FullName); \ No newline at end of file +internal record StatementHandler(StatementInformation Attribute, Action Handler, string FullName); \ No newline at end of file diff --git a/src/YesNt.Interpreter/Runtime/StatementInformation.cs b/src/YesNt.Interpreter/Runtime/StatementInformation.cs deleted file mode 100644 index 5e522b1..0000000 --- a/src/YesNt.Interpreter/Runtime/StatementInformation.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -using YesNt.Interpreter.Enums; - -namespace YesNt.Interpreter.Runtime; - -/// -/// A read-only snapshot of a registered statement's metadata, used for tooling such as -/// syntax highlighters. Instances are obtained from . -/// -public class StatementInformation -{ - /// Gets the keyword that identifies this statement in source code. - public string Name { get; internal set; } - - /// Gets where in the line the keyword is searched for. - public SearchMode SearchMode { get; internal set; } - - /// Gets which sides of the keyword must be padded with a space. - public SpaceAround SpaceAround { get; internal set; } - - /// Gets the syntax-highlight color for this statement. - public ConsoleColor Color { get; internal set; } - - /// - /// Gets a value indicating whether this statement is excluded from syntax highlighting. - /// - public bool IgnoreSyntaxHighlighting { get; internal set; } - - /// - /// Gets the optional sub-string that must be present in the line for this statement to match, - /// or if no separator is required. - /// - public string Separator { get; set; } -} \ No newline at end of file diff --git a/src/YesNt.Interpreter/Runtime/YesNtInterpreter.cs b/src/YesNt.Interpreter/Runtime/YesNtInterpreter.cs index c7be90b..813c34d 100644 --- a/src/YesNt.Interpreter/Runtime/YesNtInterpreter.cs +++ b/src/YesNt.Interpreter/Runtime/YesNtInterpreter.cs @@ -29,36 +29,17 @@ public class YesNtInterpreter public event Action OnDebugOutput; private readonly RuntimeInformation runtimeInfo = new RuntimeInformation(); - private Dictionary> statements; + private Dictionary> statements; private List statementHandlers; private List> lineMatchingHandlers = []; - private readonly List> staticStatements; - private readonly Dictionary>>> disabledStatements = []; + private readonly List> staticStatements; + private readonly Dictionary>>> disabledStatements = []; /// /// Gets a read-only snapshot of all currently registered statements. /// Useful for building syntax highlighters or documentation tools. /// - public ReadOnlyCollection StatementInformation - { - get - { - List information = statements.Select(s => - { - return new StatementInformation() - { - Name = s.Key.Name, - SearchMode = s.Key.SearchMode, - SpaceAround = s.Key.SpaceAround, - Color = s.Key.Color, - IgnoreSyntaxHighlighting = s.Key.IgnoreSyntaxHighlighting, - Separator = s.Key.Separator - }; - }).ToList(); - - return new ReadOnlyCollection(information); - } - } + public ReadOnlyCollection StatementInformation => statements.Keys.ToList().AsReadOnly(); /// /// from the moment (or any @@ -96,7 +77,7 @@ public class YesNtInterpreter } /// - /// Registers a custom statement using a pre-built . + /// Registers a custom statement using a pre-built . /// If a statement with the same attribute key (identical field values) already exists it will be replaced; /// otherwise a new entry is added. Built-in statements use distinct attribute instances, so passing a /// newly constructed attribute with the same name will add a second handler rather than replacing @@ -106,13 +87,13 @@ public class YesNtInterpreter /// The attribute describing the keyword, search mode, and priority. /// /// The delegate invoked when the statement matches. Receives the argument text - /// (the part of the line after the keyword, unless is set). + /// (the part of the line after the keyword, unless is set). /// - public void AddStatement(StatementAttributeContainer attribute, Action handler) + public void AddStatement(StatementInformation attribute, Action handler) { statements[attribute] = handler; - List>> entries = [.. statements]; + List>> entries = [.. statements]; entries.Sort((a, b) => { int cmp = a.Key.Priority.CompareTo(b.Key.Priority); @@ -121,7 +102,7 @@ public class YesNtInterpreter statements = []; - foreach (KeyValuePair> entry in entries) + foreach (KeyValuePair> entry in entries) { statements.Add(entry.Key, entry.Value); } @@ -139,7 +120,7 @@ public class YesNtInterpreter /// The delegate invoked when the statement matches. Receives the argument text and the current /// for reading/writing script state. /// - public void AddStatement(StatementAttributeContainer attribute, Action handler) + public void AddStatement(StatementInformation attribute, Action handler) { AddStatement(attribute, args => handler(args, runtimeInfo)); } @@ -153,7 +134,7 @@ public class YesNtInterpreter /// The delegate invoked when the statement matches. public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action handler) { - AddStatement(new StatementAttributeContainer(name, searchMode, spaceAround), handler); + AddStatement(new StatementInformation(name, searchMode, spaceAround), handler); } /// @@ -169,7 +150,7 @@ public class YesNtInterpreter /// public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action handler) { - AddStatement(new StatementAttributeContainer(name, searchMode, spaceAround), handler); + AddStatement(new StatementInformation(name, searchMode, spaceAround), handler); } /// @@ -182,7 +163,7 @@ public class YesNtInterpreter /// The delegate invoked when the statement matches. public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor consoleColor, Action handler) { - AddStatement(new StatementAttributeContainer(name, searchMode, spaceAround, consoleColor), handler); + AddStatement(new StatementInformation(name, searchMode, spaceAround, consoleColor), handler); } /// @@ -199,7 +180,7 @@ public class YesNtInterpreter /// public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor consoleColor, Action handler) { - AddStatement(new StatementAttributeContainer(name, searchMode, spaceAround, consoleColor), handler); + AddStatement(new StatementInformation(name, searchMode, spaceAround, consoleColor), handler); } /// @@ -208,7 +189,7 @@ public class YesNtInterpreter /// The keyword to remove. public void RemoveStatement(string name) { - foreach (StatementAttributeContainer key in statements.Keys.Where(k => k.Name == name).ToList()) + foreach (StatementInformation key in statements.Keys.Where(k => k.Name == name).ToList()) { _ = statements.Remove(key); } @@ -231,7 +212,7 @@ public class YesNtInterpreter return; } - List>> matching = + List>> matching = statements.Where(kv => kv.Key.Name == name).ToList(); if (matching.Count == 0) @@ -241,7 +222,7 @@ public class YesNtInterpreter disabledStatements[name] = matching; - foreach (KeyValuePair> kv in matching) + foreach (KeyValuePair> kv in matching) { statements[kv.Key] = _ => { }; } @@ -257,12 +238,12 @@ public class YesNtInterpreter /// The keyword of the statement(s) to re-enable. public void EnableStatement(string name) { - if (!disabledStatements.TryGetValue(name, out List>> saved)) + if (!disabledStatements.TryGetValue(name, out List>> saved)) { return; } - foreach (KeyValuePair> kv in saved) + foreach (KeyValuePair> kv in saved) { statements[kv.Key] = kv.Value; } @@ -479,7 +460,7 @@ public class YesNtInterpreter }; } - foreach (KeyValuePair staticStatement in staticStatements) + foreach (KeyValuePair staticStatement in staticStatements) { if (!staticStatement.Key.ExecuteInSearchMode && runtimeInfo.IsSearching) { @@ -498,7 +479,7 @@ public class YesNtInterpreter foreach (StatementHandler handler in handlers) { - StatementAttributeContainer statementAttribute = handler.Attribute; + StatementInformation statementAttribute = handler.Attribute; if (!statementAttribute.ExecuteInSearchMode && runtimeInfo.IsSearching) { @@ -681,7 +662,7 @@ public class YesNtInterpreter private static bool IsPossibleMatch(string content, StatementHandler handler) { - StatementAttributeContainer attr = handler.Attribute; + StatementInformation attr = handler.Attribute; string fullName = handler.FullName; return attr.SearchMode switch From 73cb4ca66952a6ecaed60d652a1281b0032f9558 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:51:40 +0200 Subject: [PATCH 05/10] Include public properties without setters from in code generator logic --- src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs b/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs index ee28b58..fd60fd6 100644 --- a/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs +++ b/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs @@ -133,7 +133,6 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator .OfType() .Where(p => !p.IsStatic && p.DeclaredAccessibility == Accessibility.Public - && p.SetMethod is not null && !ctorParamNames.Contains(p.Name)) .ToList(); From 13d85dbd054192e47364778564b20695688e0051 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Thu, 11 Jun 2026 15:24:04 +0200 Subject: [PATCH 06/10] Add property initializer default value support in generated classes --- .../StatementRegistryGenerator.cs | 91 +++++++++++++++++-- 1 file changed, 81 insertions(+), 10 deletions(-) diff --git a/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs b/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs index fd60fd6..eda18db 100644 --- a/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs +++ b/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs @@ -1,4 +1,5 @@ using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; using System; using System.Collections.Generic; @@ -30,7 +31,7 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator INamedTypeSymbol? statementAttrSymbol = compilation.GetTypeByMetadataName(StatementAttributeName); INamedTypeSymbol? staticStatementAttrSymbol = compilation.GetTypeByMetadataName(StaticStatementAttributeName); - string infoSource = GenerateInformationClassesSource(statementAttrSymbol, staticStatementAttrSymbol); + string infoSource = GenerateInformationClassesSource(statementAttrSymbol, staticStatementAttrSymbol, compilation); context.AddSource("GeneratedStatementInformation.g.cs", infoSource); string registrySource = GenerateRegistrySource(statementMethods, staticStatementMethods); @@ -89,7 +90,8 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator private static string GenerateInformationClassesSource( INamedTypeSymbol? statementAttrSymbol, - INamedTypeSymbol? staticStatementAttrSymbol) + INamedTypeSymbol? staticStatementAttrSymbol, + Compilation compilation) { StringBuilder sb = new StringBuilder(); @@ -101,12 +103,12 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator if (statementAttrSymbol is not null) { - EmitInformationClass(sb, statementAttrSymbol, "StatementInformation"); + EmitInformationClass(sb, statementAttrSymbol, "StatementInformation", compilation); } if (staticStatementAttrSymbol is not null) { - EmitInformationClass(sb, staticStatementAttrSymbol, "StaticStatementInformation"); + EmitInformationClass(sb, staticStatementAttrSymbol, "StaticStatementInformation", compilation); } return sb.ToString(); @@ -115,7 +117,8 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator private static void EmitInformationClass( StringBuilder sb, INamedTypeSymbol attributeSymbol, - string className) + string className, + Compilation compilation) { List ctors = attributeSymbol.Constructors .Where(c => !c.IsImplicitlyDeclared) @@ -186,7 +189,7 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator // ---- settable properties (named-argument style) ---- foreach (IPropertySymbol prop in settableProps) { - string defaultClause = GetDefaultClause(prop); + string defaultClause = GetDefaultClause(prop, compilation); _ = sb.AppendLine($" public {GlobalType(prop.Type)} {prop.Name} {{ get; init; }}{defaultClause}"); } @@ -244,11 +247,47 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator return string.IsNullOrEmpty(name) ? name : char.ToUpperInvariant(name[0]) + name.Substring(1); } - private static string GetDefaultClause(IPropertySymbol prop) + private static string GetDefaultClause( + IPropertySymbol prop, + Compilation compilation) { - // Only emit a default for a handful of well-known "safe" defaults so the - // generated code compiles even when the caller omits the named argument. - return prop.Type.IsReferenceType || prop.Type.NullableAnnotation == NullableAnnotation.Annotated + foreach (SyntaxReference syntaxRef in prop.DeclaringSyntaxReferences) + { + if (syntaxRef.GetSyntax() is not Microsoft.CodeAnalysis.CSharp.Syntax.PropertyDeclarationSyntax syntax) + { + continue; + } + + if (syntax.Initializer?.Value is null) + { + continue; + } + + SemanticModel model = compilation.GetSemanticModel(syntax.SyntaxTree); + ExpressionSyntax expr = syntax.Initializer.Value; + + // 1. Try constant evaluation first (SAFE) + Optional constant = model.GetConstantValue(expr); + if (constant.HasValue) + { + return $" = {ToLiteral(constant.Value!, prop.Type)};"; + } + + // 2. Try symbol resolution (enum fields etc.) + ISymbol? symbolInfo = model.GetSymbolInfo(expr).Symbol; + + if (symbolInfo is IFieldSymbol field) + { + string typeName = GlobalType(field.ContainingType); + return $" = {typeName}.{field.Name};"; + } + + // 3. fallback: raw expression (last resort) + return $" = {expr};"; + } + + // fallback defaults + return prop.Type.IsReferenceType || prop.NullableAnnotation == NullableAnnotation.Annotated ? " = null!;" : prop.Type.SpecialType switch { @@ -391,6 +430,38 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator }; } + private static string ToLiteral(object value, ITypeSymbol type) + { + if (value is null) + { + return "null!"; + } + + if (type.TypeKind == TypeKind.Enum) + { + string enumType = GlobalType(type); + + // value is already boxed enum OR underlying integral type + long underlying = Convert.ToInt64(value); + + // fallback: cast + return $"({enumType}){underlying}"; + } + + return type.SpecialType switch + { + SpecialType.System_String => $"\"{EscapeString((string)value)}\"", + SpecialType.System_Char => $"'{EscapeChar((char)value)}'", + SpecialType.System_Boolean => (bool)value ? "true" : "false", + SpecialType.System_Int32 => ((int)value).ToString(System.Globalization.CultureInfo.InvariantCulture), + SpecialType.System_Int64 => ((long)value).ToString(System.Globalization.CultureInfo.InvariantCulture) + "L", + SpecialType.System_Single => ((float)value).ToString(System.Globalization.CultureInfo.InvariantCulture) + "f", + SpecialType.System_Double => ((double)value).ToString(System.Globalization.CultureInfo.InvariantCulture), + SpecialType.System_Decimal => ((decimal)value).ToString(System.Globalization.CultureInfo.InvariantCulture) + "m", + _ => value.ToString() ?? "null!" + }; + } + private static string EscapeString(string value) { return value From 77ffdde2efa1b0284a37a8975240c69227576d33 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:05:47 +0200 Subject: [PATCH 07/10] Make TemplateProcessor public and restrict some of its methods to internal --- src/YesNt.Interpreter/Utilities/TemplateProcessor.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/YesNt.Interpreter/Utilities/TemplateProcessor.cs b/src/YesNt.Interpreter/Utilities/TemplateProcessor.cs index 5d497bd..744ceb6 100644 --- a/src/YesNt.Interpreter/Utilities/TemplateProcessor.cs +++ b/src/YesNt.Interpreter/Utilities/TemplateProcessor.cs @@ -10,12 +10,12 @@ namespace YesNt.Interpreter.Utilities; /// /// Provides high-performance template substitution for variables and stack parameters. /// -internal static class TemplateProcessor +public static class TemplateProcessor { /// /// Replaces all occurrences of ${variableName} with their current values. /// - public static string ProcessVariables(string input, RuntimeInformation runtimeInfo) + internal static string ProcessVariables(string input, RuntimeInformation runtimeInfo) { if (string.IsNullOrEmpty(input)) { @@ -76,7 +76,7 @@ internal static class TemplateProcessor /// /// Replaces all occurrences of a placeholder (e.g., %in, %out) with values popped from a stack. /// - public static string ProcessStackParameters(string input, string placeholder, Stack stack, RuntimeInformation runtimeInfo, string emptyStackMessage) + internal static string ProcessStackParameters(string input, string placeholder, Stack stack, RuntimeInformation runtimeInfo, string emptyStackMessage) { if (string.IsNullOrEmpty(input)) { @@ -153,7 +153,7 @@ internal static class TemplateProcessor /// /// Replaces all occurrences of arithmetic expressions with their results. /// - public static string ProcessCalculations(string input, RuntimeInformation runtimeInfo, Regex calculationRegex) + internal static string ProcessCalculations(string input, RuntimeInformation runtimeInfo, Regex calculationRegex) { if (string.IsNullOrEmpty(input)) { From ff8523fe8b43cb929b10c8d586b8a42d24b078fa Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:36:52 +0200 Subject: [PATCH 08/10] Add XML doc comments for generated classes, constructors, and properties --- .../StatementRegistryGenerator.cs | 90 +++++++++++++++++-- 1 file changed, 84 insertions(+), 6 deletions(-) diff --git a/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs b/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs index eda18db..bbbad00 100644 --- a/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs +++ b/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs @@ -141,16 +141,22 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator // All properties ever assigned by any ctor, deduplicated by name. // We need a get-only property for each of them. - List<(string PropName, ITypeSymbol PropType)> ctorProps = ctors + var ctorProps = ctors .SelectMany(c => c.Parameters) - .Select(p => ( - PropName: ResolvePropertyName(attributeSymbol, p), - PropType: p.Type)) + .Select(p => { + string propName = ResolvePropertyName(attributeSymbol, p); + IPropertySymbol? propSymbol = attributeSymbol + .GetMembers() + .OfType() + .FirstOrDefault(sym => string.Equals(sym.Name, propName, StringComparison.OrdinalIgnoreCase)); + return (PropName: propName, PropType: p.Type, PropSymbol: propSymbol); + }) .GroupBy(x => x.PropName, StringComparer.OrdinalIgnoreCase) .Select(g => g.First()) .ToList(); // ---- class declaration ---- + _ = sb.Append(GetFormattedComment(attributeSymbol, attributeSymbol, className, $"Information about the {className} class.", "")); _ = sb.AppendLine($"public sealed class {className}"); _ = sb.AppendLine("{"); @@ -159,6 +165,7 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator { if (ctor.Parameters.Length == 0) { + _ = sb.Append(GetFormattedComment(ctor, attributeSymbol, className, $"Initializes a new instance of the class.", " ")); _ = sb.AppendLine($" public {className}() {{ }}"); _ = sb.AppendLine(); continue; @@ -167,6 +174,7 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator string ctorParams = string.Join(", ", ctor.Parameters.Select(p => $"{GlobalType(p.Type)} {p.Name}")); + _ = sb.Append(GetFormattedComment(ctor, attributeSymbol, className, $"Initializes a new instance of the class.", " ")); _ = sb.AppendLine($" public {className}({ctorParams})"); _ = sb.AppendLine(" {"); @@ -181,15 +189,26 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator } // ---- get-only properties sourced from ctor parameters ---- - foreach ((string propName, ITypeSymbol propType) in ctorProps) + foreach (var ctorProp in ctorProps) { - _ = sb.AppendLine($" public {GlobalType(propType)} {propName} {{ get; }}"); + if (ctorProp.PropSymbol is not null) + { + _ = sb.Append(GetFormattedComment(ctorProp.PropSymbol, attributeSymbol, className, $"Gets the {ctorProp.PropName} property.", " ")); + } + else + { + _ = sb.AppendLine(" /// "); + _ = sb.AppendLine($" /// Gets the {ctorProp.PropName} property."); + _ = sb.AppendLine(" /// "); + } + _ = sb.AppendLine($" public {GlobalType(ctorProp.PropType)} {ctorProp.PropName} {{ get; }}"); } // ---- settable properties (named-argument style) ---- foreach (IPropertySymbol prop in settableProps) { string defaultClause = GetDefaultClause(prop, compilation); + _ = sb.Append(GetFormattedComment(prop, attributeSymbol, className, $"Gets or sets the {prop.Name} property.", " ")); _ = sb.AppendLine($" public {GlobalType(prop.Type)} {prop.Name} {{ get; init; }}{defaultClause}"); } @@ -197,6 +216,65 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator _ = sb.AppendLine(); } + private static string GetFormattedComment( + ISymbol symbol, + INamedTypeSymbol attributeSymbol, + string className, + string fallbackSummary, + string indent = " ") + { + string? xml = symbol.GetDocumentationCommentXml(); + if (!string.IsNullOrEmpty(xml)) + { + xml = xml!.Replace(attributeSymbol.Name, className); + } + + string formatted = FormatXmlComment(xml, indent); + if (!string.IsNullOrWhiteSpace(formatted)) + { + return formatted; + } + + // Fallback + var sb = new StringBuilder(); + _ = sb.AppendLine($"{indent}/// "); + _ = sb.AppendLine($"{indent}/// {fallbackSummary}"); + _ = sb.AppendLine($"{indent}/// "); + return sb.ToString(); + } + + private static string FormatXmlComment(string? xml, string indent) + { + if (string.IsNullOrWhiteSpace(xml)) + { + return string.Empty; + } + + try + { + var element = System.Xml.Linq.XElement.Parse(xml!); + var sb = new StringBuilder(); + foreach (var child in element.Elements()) + { + string nodeXml = child.ToString(); + string[] lines = nodeXml.Split(new[] { "\r\n", "\r", "\n" }, StringSplitOptions.None); + foreach (string line in lines) + { + string trimmedLine = line.Trim(); + if (trimmedLine.Length > 0) + { + _ = sb.AppendLine($"{indent}/// {trimmedLine}"); + } + } + } + return sb.ToString(); + } + catch + { + return string.Empty; + } + } + // Finds the attribute property that corresponds to a constructor parameter. private static string ResolvePropertyName(INamedTypeSymbol attributeSymbol, IParameterSymbol parameter) { From c39a1d3ff442983af6c21607f11f7dbcd686f8cd Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:45:58 +0200 Subject: [PATCH 09/10] Update package and project version to 1.1.0.0 --- choco/yesnt.nuspec | 2 +- src/YesNt.Interpreter/YesNt.Interpreter.csproj | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/choco/yesnt.nuspec b/choco/yesnt.nuspec index 6b0e16e..99c8a28 100644 --- a/choco/yesnt.nuspec +++ b/choco/yesnt.nuspec @@ -2,7 +2,7 @@ yesnt - 1.0.0.0 + 1.1.0.0 YesNt Stone_Red https://github.com/Stone-Red-Code/YesNt-Interpreter diff --git a/src/YesNt.Interpreter/YesNt.Interpreter.csproj b/src/YesNt.Interpreter/YesNt.Interpreter.csproj index 8d632be..fd22190 100644 --- a/src/YesNt.Interpreter/YesNt.Interpreter.csproj +++ b/src/YesNt.Interpreter/YesNt.Interpreter.csproj @@ -14,6 +14,7 @@ Logo.png True LICENSE + 1.1.0.0 From d09e054f8fcb50deccd29e9b17d473582fa6a7ef Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Thu, 11 Jun 2026 19:46:04 +0200 Subject: [PATCH 10/10] Add stepwise and time-budgeted execution section and update API docs --- docs/library-api.md | 93 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 84 insertions(+), 9 deletions(-) diff --git a/docs/library-api.md b/docs/library-api.md index 256d9da..f3ba942 100644 --- a/docs/library-api.md +++ b/docs/library-api.md @@ -88,6 +88,47 @@ interpreter.Execute(lines); --- +## Stepwise and time-budgeted execution + +For interactive environments, game loops, or time-sliced applications, you can prepare a script and run it incrementally. + +### Preparing the interpreter +Call `Prepare` instead of `Execute` to load the script without running it immediately: + +```csharp +var interpreter = new YesNtInterpreter(); +interpreter.Prepare("path/to/script.ynt"); +// or: interpreter.Prepare(lines); +``` + +### Driving execution +Use `IsRunning` to check if there are more lines to execute, and step by line count or run with a time budget: + +```csharp +// Execute 5 lines of the script +StepResult result = interpreter.Step(5); + +if (result == StepResult.Paused) +{ + // The line budget was exhausted; resume execution later +} +``` + +Or run the interpreter with a wall-clock time limit (useful for preventing freezing in game loops): + +```csharp +// Run for up to 10 milliseconds +StepResult result = interpreter.RunFor(TimeSpan.FromMilliseconds(10)); +``` + +You can also run the remaining script to completion: + +```csharp +interpreter.RunToCompletion(); +``` + +--- + ## Capturing output (debug mode) Pass `isDebugMode: true` to suppress direct console writes. Output is delivered through the @@ -186,12 +227,12 @@ interpreter.AddStatement("log", SearchMode.StartOfLine, SpaceAround.End, args => Console.WriteLine($"[LOG] {args}")); ``` -### Using a `StatementAttribute` +### Using a `StatementInformation` ```csharp -using YesNt.Interpreter.Attributes; +using YesNt.Interpreter.Runtime; -var attr = new StatementAttribute("log", SearchMode.StartOfLine, SpaceAround.End) +var attr = new StatementInformation("log", SearchMode.StartOfLine, SpaceAround.End) { Priority = Priority.VeryLow, }; @@ -218,7 +259,7 @@ interpreter.AddStatement(attr, args => Console.WriteLine($"[LOG] {args}")); | `StartEnd` | Spaces required on both sides | Custom statements run at `Priority.Normal` by default. Statements with a higher-ranking enum member (`PreProcessing` → `Highest` → … → `VeryLow`) run first; `VeryLow` runs last. -Use `StatementAttribute.Priority` to control ordering relative to built-in statements. +Use `StatementInformation.Priority` to control ordering relative to built-in statements. --- @@ -370,15 +411,30 @@ public event Action OnWaitingForInput; #### Methods ```csharp -// Execute a .ynt file +// Execute a .ynt file to completion public void Execute(string path, bool isDebugMode = false); -// Execute in-memory lines -public void Execute(List lines, bool isDebugMode = false); +// Execute in-memory lines to completion +public void Execute(IEnumerable lines, bool isDebugMode = false); + +// Prepare a script file for stepwise execution +public void Prepare(string path, bool isDebugMode = false); + +// Prepare in-memory lines for stepwise execution +public void Prepare(IEnumerable lines, bool isDebugMode = false); + +// Execute up to standard line count then pause +public StepResult Step(int lines = 1); + +// Run the script for up to budget duration then pause +public StepResult RunFor(TimeSpan budget); + +// Execute the remaining script lines to completion +public void RunToCompletion(); // Register a custom statement (full control) -public void AddStatement(StatementAttribute attribute, Action handler); -public void AddStatement(StatementAttribute attribute, Action handler); +public void AddStatement(StatementInformation attribute, Action handler); +public void AddStatement(StatementInformation attribute, Action handler); // Register a custom statement (convenience overloads) public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action handler); @@ -404,6 +460,9 @@ public void Stop(); ```csharp // Read-only snapshot of all registered statements public ReadOnlyCollection StatementInformation { get; } + +// Whether a prepared script is currently active/running +public bool IsRunning { get; } ``` --- @@ -424,3 +483,19 @@ Provides access to the script state that a built-in statement handler would have | `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 | + +--- + +### `StepResult` + +```csharp +public enum StepResult // YesNt.Interpreter.Runtime +``` + +Returned by `Step` and `RunFor` to indicate the outcome of the incremental execution. + +| Value | Description | +| ---------- | -------------------------------------------------------------- | +| `Continue` | A line was executed and more lines remain. | +| `Paused` | The step or time budget was exhausted before the script ended. | +| `Finished` | The script ran to completion (or terminated/exited). |