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/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). | diff --git a/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs b/src/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs index 242902d..bbbad00 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; @@ -27,8 +28,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, compilation); + context.AddSource("GeneratedStatementInformation.g.cs", infoSource); + + string registrySource = GenerateRegistrySource(statementMethods, staticStatementMethods); + context.AddSource("GeneratedStatementRegistry.g.cs", registrySource); } private static void CollectMethods( @@ -77,6 +84,302 @@ public sealed class StatementRegistryGenerator : IIncrementalGenerator } } + // ------------------------------------------------------------------------- + // Information class generation + // ------------------------------------------------------------------------- + + private static string GenerateInformationClassesSource( + INamedTypeSymbol? statementAttrSymbol, + INamedTypeSymbol? staticStatementAttrSymbol, + Compilation compilation) + { + 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", compilation); + } + + if (staticStatementAttrSymbol is not null) + { + EmitInformationClass(sb, staticStatementAttrSymbol, "StaticStatementInformation", compilation); + } + + return sb.ToString(); + } + + private static void EmitInformationClass( + StringBuilder sb, + INamedTypeSymbol attributeSymbol, + string className, + Compilation compilation) + { + 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 + && !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. + var ctorProps = ctors + .SelectMany(c => c.Parameters) + .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("{"); + + // ---- one constructor per attribute constructor ---- + foreach (IMethodSymbol ctor in ctors) + { + 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; + } + + 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(" {"); + + 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 (var ctorProp in ctorProps) + { + 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}"); + } + + _ = sb.AppendLine("}"); + _ = 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) + { + 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, + Compilation compilation) + { + 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 + { + 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) @@ -87,7 +390,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 +397,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 +419,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.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.StaticStatementAttribute", method.Attribute); + string attributeCreation = BuildAttributeCreation("global::YesNt.Interpreter.Runtime.StaticStatementInformation", 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("}"); @@ -203,6 +508,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 @@ -234,4 +571,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 f604839..1aeb9d2 100644 --- a/src/YesNt.Interpreter.Tests/AddStatementTests.cs +++ b/src/YesNt.Interpreter.Tests/AddStatementTests.cs @@ -2,7 +2,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Collections.Generic; -using YesNt.Interpreter.Attributes; using YesNt.Interpreter.Enums; using YesNt.Interpreter.Runtime; @@ -65,7 +64,7 @@ public class AddStatementTests _ = YesNtAssert.GetLastLineWithSetup(lines, interpreter => { - StatementAttribute attr = new StatementAttribute("attr_cmd", SearchMode.StartOfLine, SpaceAround.End); + StatementInformation attr = new StatementInformation("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 StatementInformation("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 StatementInformation("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/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 1a2d66c..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(StatementAttribute 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/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 2707aad..813c34d 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; @@ -28,36 +29,24 @@ 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(); + public ReadOnlyCollection StatementInformation => statements.Keys.ToList().AsReadOnly(); - return new ReadOnlyCollection(information); - } - } + /// + /// 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. @@ -88,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 @@ -98,15 +87,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(StatementInformation 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 +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(StatementAttribute attribute, Action handler) + public void AddStatement(StatementInformation attribute, Action handler) { AddStatement(attribute, args => handler(args, runtimeInfo)); } @@ -134,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 StatementAttribute(name, searchMode, spaceAround), handler); + AddStatement(new StatementInformation(name, searchMode, spaceAround), handler); } /// @@ -150,7 +150,7 @@ public class YesNtInterpreter /// public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action handler) { - AddStatement(new StatementAttribute(name, searchMode, spaceAround), handler); + AddStatement(new StatementInformation(name, searchMode, spaceAround), handler); } /// @@ -163,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 StatementAttribute(name, searchMode, spaceAround, consoleColor), handler); + AddStatement(new StatementInformation(name, searchMode, spaceAround, consoleColor), handler); } /// @@ -180,7 +180,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 StatementInformation(name, searchMode, spaceAround, consoleColor), handler); } /// @@ -189,7 +189,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 (StatementInformation key in statements.Keys.Where(k => k.Name == name).ToList()) { _ = statements.Remove(key); } @@ -212,7 +212,7 @@ public class YesNtInterpreter return; } - List>> matching = + List>> matching = statements.Where(kv => kv.Key.Name == name).ToList(); if (matching.Count == 0) @@ -222,7 +222,7 @@ public class YesNtInterpreter disabledStatements[name] = matching; - foreach (KeyValuePair> kv in matching) + foreach (KeyValuePair> kv in matching) { statements[kv.Key] = _ => { }; } @@ -238,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; } @@ -263,7 +263,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. /// @@ -272,35 +318,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) @@ -311,22 +344,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]; @@ -334,9 +443,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) { @@ -349,10 +460,9 @@ public class YesNtInterpreter }; } - foreach (KeyValuePair staticStatement in staticStatements) + foreach (KeyValuePair staticStatement in staticStatements) { - StaticStatementAttribute staticStatementAttribute = staticStatement.Key; - if (!staticStatementAttribute.ExecuteInSearchMode && runtimeInfo.IsSearching) + if (!staticStatement.Key.ExecuteInSearchMode && runtimeInfo.IsSearching) { continue; } @@ -363,11 +473,13 @@ 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) { - StatementAttribute statementAttribute = handler.Attribute; + StatementInformation statementAttribute = handler.Attribute; if (!statementAttribute.ExecuteInSearchMode && runtimeInfo.IsSearching) { @@ -414,13 +526,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)) @@ -435,12 +561,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) @@ -534,7 +662,7 @@ public class YesNtInterpreter private static bool IsPossibleMatch(string content, StatementHandler handler) { - StatementAttribute attr = handler.Attribute; + StatementInformation attr = handler.Attribute; string fullName = handler.FullName; return attr.SearchMode switch 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(); } 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)) { 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