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