From 13ef994d8b7f8f7a5db2730fc8214a7e5fe156ff Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Thu, 5 Mar 2026 22:39:00 +0100 Subject: [PATCH] Performance improvements and code cleanup --- .../Attributes/StatementAttribute.cs | 22 +- YesNt.Interpreter/Runtime/Line.cs | 4 +- .../Runtime/RuntimeInformation.cs | 6 +- YesNt.Interpreter/Runtime/StatementHandler.cs | 9 + .../Runtime/StatementRuntimeInfo.cs | 8 + YesNt.Interpreter/Runtime/YesNtInterpreter.cs | 210 +++++++++++++----- .../Statements/CodeFlowStatements.cs | 171 ++------------ .../Statements/FunctionStatements.cs | 48 +--- .../PredefinedVariableStatements.cs | 30 +-- .../Statements/ProcessingStatements.cs | 16 +- .../Statements/VariableStatements.cs | 75 +------ YesNt.Interpreter/Utilities/Evaluator.cs | 43 ++-- YesNt.Interpreter/Utilities/FixedProcess.cs | 2 +- .../Utilities/StringExtensions.cs | 81 ++++--- .../Utilities/TemplateProcessor.cs | 167 ++++++++++++++ 15 files changed, 488 insertions(+), 404 deletions(-) create mode 100644 YesNt.Interpreter/Runtime/StatementHandler.cs create mode 100644 YesNt.Interpreter/Utilities/TemplateProcessor.cs diff --git a/YesNt.Interpreter/Attributes/StatementAttribute.cs b/YesNt.Interpreter/Attributes/StatementAttribute.cs index 9aded1f..154936e 100644 --- a/YesNt.Interpreter/Attributes/StatementAttribute.cs +++ b/YesNt.Interpreter/Attributes/StatementAttribute.cs @@ -1,4 +1,4 @@ -using System; +using System; using YesNt.Interpreter.Enums; @@ -60,6 +60,24 @@ public class StatementAttribute : Attribute /// 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. /// @@ -89,4 +107,4 @@ public class StatementAttribute : Attribute SpaceAround = spaceAround; IgnoreSyntaxHighlighting = true; } -} \ No newline at end of file +} diff --git a/YesNt.Interpreter/Runtime/Line.cs b/YesNt.Interpreter/Runtime/Line.cs index fcb53af..03f7337 100644 --- a/YesNt.Interpreter/Runtime/Line.cs +++ b/YesNt.Interpreter/Runtime/Line.cs @@ -1,4 +1,6 @@ -namespace YesNt.Interpreter.Runtime; +using System.Collections.Generic; + +namespace YesNt.Interpreter.Runtime; /// /// Represents a single source line together with its location metadata. diff --git a/YesNt.Interpreter/Runtime/RuntimeInformation.cs b/YesNt.Interpreter/Runtime/RuntimeInformation.cs index 306f40a..742674b 100644 --- a/YesNt.Interpreter/Runtime/RuntimeInformation.cs +++ b/YesNt.Interpreter/Runtime/RuntimeInformation.cs @@ -26,6 +26,8 @@ internal sealed class RuntimeInformation public Dictionary GlobalVariables { get; set; } = []; public Dictionary Functions { get; } = []; + public Dictionary BlockBoundaries { get; } = []; + internal Action PreScanLinesAction { get; set; } public Stack FunctionCallStack { get; } = new(); public Stack InParametersStack { get; } = new(); public Stack OutParametersStack { get; set; } = new(); @@ -173,6 +175,7 @@ internal sealed class RuntimeInformation GlobalVariables.Clear(); Labels.Clear(); Functions.Clear(); + BlockBoundaries.Clear(); FunctionCallStack.Clear(); InParametersStack.Clear(); OutParametersStack.Clear(); @@ -187,8 +190,7 @@ internal sealed class RuntimeInformation IsInFunction = false; IsLocalSearch = false; LineNumber = 0; - TaskId = internalTaskId + 1; - internalTaskId++; + TaskId = ++internalTaskId; } private void ParentRuntimeInformation_OnExit(string exitMessage, bool stopAllTasks) diff --git a/YesNt.Interpreter/Runtime/StatementHandler.cs b/YesNt.Interpreter/Runtime/StatementHandler.cs new file mode 100644 index 0000000..710a8e1 --- /dev/null +++ b/YesNt.Interpreter/Runtime/StatementHandler.cs @@ -0,0 +1,9 @@ +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); diff --git a/YesNt.Interpreter/Runtime/StatementRuntimeInfo.cs b/YesNt.Interpreter/Runtime/StatementRuntimeInfo.cs index a8b89e7..706456c 100644 --- a/YesNt.Interpreter/Runtime/StatementRuntimeInfo.cs +++ b/YesNt.Interpreter/Runtime/StatementRuntimeInfo.cs @@ -13,4 +13,12 @@ internal abstract class StatementRuntimeInformation /// Injected by the generated registry before any handler is invoked. /// public RuntimeInformation RuntimeInfo { get; set; } + + /// + /// Trims surrounding whitespace and a trailing colon from a block or function name. + /// + protected static string NormalizeBlockName(string value) + { + return value.Trim().TrimEnd(':').Trim(); + } } \ No newline at end of file diff --git a/YesNt.Interpreter/Runtime/YesNtInterpreter.cs b/YesNt.Interpreter/Runtime/YesNtInterpreter.cs index e918c78..c76f0ae 100644 --- a/YesNt.Interpreter/Runtime/YesNtInterpreter.cs +++ b/YesNt.Interpreter/Runtime/YesNtInterpreter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; @@ -13,20 +13,6 @@ namespace YesNt.Interpreter.Runtime; /// /// The main entry point for executing YesNt scripts. /// -/// -/// Running a script file: -/// -/// var interpreter = new YesNtInterpreter(); -/// interpreter.Execute("path/to/script.ynt"); -/// -/// Running script lines in memory with a custom statement: -/// -/// var interpreter = new YesNtInterpreter(); -/// interpreter.AddStatement("log", SearchMode.StartOfLine, SpaceAround.End, args => -/// Console.WriteLine($"[LOG] {args}")); -/// interpreter.Execute(new List<string> { "log hello world" }); -/// -/// public class YesNtInterpreter { /// @@ -43,6 +29,8 @@ public class YesNtInterpreter private readonly RuntimeInformation runtimeInfo = new RuntimeInformation(); private Dictionary> statements; + private List statementHandlers; + private List> lineMatchingHandlers = []; private readonly List> staticStatements; private readonly Dictionary>>> disabledStatements = []; @@ -77,11 +65,28 @@ public class YesNtInterpreter public YesNtInterpreter() { GeneratedStatementRegistry.Register(runtimeInfo, out statements, out staticStatements); + UpdateStatementHandlers(); + runtimeInfo.PreScanLinesAction = PreScanLines; runtimeInfo.OnDebugOutput += (s) => OnDebugOutput?.Invoke(s); runtimeInfo.OnLineExecuted += e => OnLineExecuted?.Invoke(e); } + private void UpdateStatementHandlers() + { + statementHandlers = statements.Select(s => + { + string name = s.Key.SpaceAround switch + { + SpaceAround.StartEnd => $" {s.Key.Name.Trim()} ", + SpaceAround.Start => $" {s.Key.Name.Trim()}", + SpaceAround.End => $"{s.Key.Name.Trim()} ", + _ => s.Key.Name.Trim() + }; + return new StatementHandler(s.Key, s.Value, name); + }).ToList(); + } + /// /// 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; @@ -102,14 +107,16 @@ public class YesNtInterpreter .OrderBy(s => s.Key.Priority) .ThenByDescending(s => s.Key.Name.Length) .ToDictionary(x => x.Key, x => x.Value); + UpdateStatementHandlers(); + PreScanLines(); } /// - /// Registers a custom statement without a syntax-highlight color. + /// Registers a simple custom statement with default settings. /// - /// The keyword that identifies this statement in source code. - /// Where in the line the keyword is matched. - /// Which sides of the keyword require a surrounding space. + /// The keyword to match. + /// Where in the line the keyword is searched for. + /// Which sides of the keyword must be padded with a space. /// The delegate invoked when the statement matches. public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action handler) { @@ -117,12 +124,12 @@ public class YesNtInterpreter } /// - /// Registers a custom statement with a syntax-highlight color. + /// Registers a simple custom statement with a specific syntax-highlight color. /// - /// The keyword that identifies this statement in source code. - /// 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. + /// The keyword to match. + /// Where in the line the keyword is searched for. + /// Which sides of the keyword must be padded with a space. + /// The color used for syntax highlighting. /// The delegate invoked when the statement matches. public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor consoleColor, Action handler) { @@ -130,10 +137,9 @@ public class YesNtInterpreter } /// - /// Permanently removes all built-in or custom statements that match . - /// After removal, any script line that would have matched triggers an "Invalid statement" error. + /// Unregisters all handlers matching the specified keyword . /// - /// The keyword of the statement(s) to remove. + /// The keyword to remove. public void RemoveStatement(string name) { foreach (StatementAttribute key in statements.Keys.Where(k => k.Name == name).ToList()) @@ -142,6 +148,8 @@ public class YesNtInterpreter } _ = disabledStatements.Remove(name); + UpdateStatementHandlers(); + PreScanLines(); } /// @@ -171,6 +179,8 @@ public class YesNtInterpreter { statements[kv.Key] = _ => { }; } + UpdateStatementHandlers(); + PreScanLines(); } /// @@ -192,6 +202,8 @@ public class YesNtInterpreter } _ = disabledStatements.Remove(name); + UpdateStatementHandlers(); + PreScanLines(); } /// @@ -236,9 +248,11 @@ public class YesNtInterpreter for (int i = 0; i < lines.Count; i++) { - runtimeInfo.Lines.Add(new Line(lines[i], Path.GetFileName("#Memory#"), i)); + string content = lines[i].Trim().Replace("\r", string.Empty); + runtimeInfo.Lines.Add(new Line(content, Path.GetFileName("#Memory#"), i)); } + PreScanLines(); Execute(); } @@ -255,6 +269,7 @@ public class YesNtInterpreter runtimeInfo.Exit(ExitMessages.TerminatedByParentTask, parentRuntimeInformation.StopAllTasks); return; } + PreScanLines(); Execute(); } @@ -267,20 +282,25 @@ public class YesNtInterpreter break; } - runtimeInfo.CurrentLine = runtimeInfo.Lines[runtimeInfo.LineNumber].Content.Trim(' ').Replace("\r", string.Empty); + Line lineObj = runtimeInfo.Lines[runtimeInfo.LineNumber]; + runtimeInfo.CurrentLine = lineObj.Content; if (string.IsNullOrWhiteSpace(runtimeInfo.CurrentLine) || runtimeInfo.CurrentLine.StartsWith('#')) { continue; } - DebugEventArgs debugEventArgs = new DebugEventArgs() + DebugEventArgs debugEventArgs = null; + if (runtimeInfo.IsDebugMode) { - LineNumber = runtimeInfo.LineNumber + 1, - OriginalLine = runtimeInfo.CurrentLine.FromSafeString(), - IsTask = runtimeInfo.IsTask, - TaskId = runtimeInfo.TaskId - }; + debugEventArgs = new DebugEventArgs() + { + LineNumber = runtimeInfo.LineNumber + 1, + OriginalLine = runtimeInfo.CurrentLine.FromSafeString(), + IsTask = runtimeInfo.IsTask, + TaskId = runtimeInfo.TaskId + }; + } foreach (KeyValuePair staticStatement in staticStatements) { @@ -296,9 +316,11 @@ public class YesNtInterpreter bool statementFound = false; bool notSearchingLabel = !runtimeInfo.IsSearching; - foreach (KeyValuePair> statement in statements) + List handlers = (runtimeInfo.LineNumber < lineMatchingHandlers.Count) ? lineMatchingHandlers[runtimeInfo.LineNumber] : []; + + foreach (StatementHandler handler in handlers) { - StatementAttribute statementAttribute = statement.Key; + StatementAttribute statementAttribute = handler.Attribute; if (!statementAttribute.ExecuteInSearchMode && runtimeInfo.IsSearching) { @@ -311,37 +333,31 @@ public class YesNtInterpreter break; } - string name = statementAttribute.SpaceAround switch - { - SpaceAround.StartEnd => $" {statementAttribute.Name.Trim()} ", - SpaceAround.Start => $" {statementAttribute.Name.Trim()}", - SpaceAround.End => $"{statementAttribute.Name.Trim()} ", - _ => statementAttribute.Name.Trim() - }; + string name = handler.FullName; - if (statementAttribute.Separator is null || runtimeInfo.CurrentLine.Contains(statementAttribute.Separator)) + if (statementAttribute.Separator is null || runtimeInfo.CurrentLine.Contains(statementAttribute.Separator, StringComparison.Ordinal)) { - if (statementAttribute.SearchMode == SearchMode.StartOfLine && runtimeInfo.CurrentLine.StartsWith(name)) + if (statementAttribute.SearchMode == SearchMode.StartOfLine && runtimeInfo.CurrentLine.StartsWith(name, StringComparison.Ordinal)) { string copyLine = statementAttribute.KeepStatementInArgs ? runtimeInfo.CurrentLine : runtimeInfo.CurrentLine[name.Length..]; - statement.Value.Invoke(copyLine); + handler.Handler.Invoke(copyLine); statementFound = true; } - else if (statementAttribute.SearchMode == SearchMode.Contains && runtimeInfo.CurrentLine.Contains(name)) + else if (statementAttribute.SearchMode == SearchMode.Contains && runtimeInfo.CurrentLine.Contains(name, StringComparison.Ordinal)) { string copyLine = statementAttribute.KeepStatementInArgs ? runtimeInfo.CurrentLine : runtimeInfo.CurrentLine.Replace(name, string.Empty); - statement.Value.Invoke(copyLine); + handler.Handler.Invoke(copyLine); statementFound = true; } - else if (statementAttribute.SearchMode == SearchMode.EndOfLine && runtimeInfo.CurrentLine.EndsWith(name)) + else if (statementAttribute.SearchMode == SearchMode.EndOfLine && runtimeInfo.CurrentLine.EndsWith(name, StringComparison.Ordinal)) { string copyLine = statementAttribute.KeepStatementInArgs ? runtimeInfo.CurrentLine : runtimeInfo.CurrentLine[..^name.Length]; - statement.Value.Invoke(copyLine); + handler.Handler.Invoke(copyLine); statementFound = true; } - else if (statementAttribute.SearchMode == SearchMode.Exact && runtimeInfo.CurrentLine.Equals(name)) + else if (statementAttribute.SearchMode == SearchMode.Exact && runtimeInfo.CurrentLine.Equals(name, StringComparison.Ordinal)) { - statement.Value.Invoke(runtimeInfo.CurrentLine); + handler.Handler.Invoke(runtimeInfo.CurrentLine); statementFound = true; } } @@ -351,7 +367,7 @@ public class YesNtInterpreter { runtimeInfo.Exit(ExitMessages.InvalidStatement, true); } - if (runtimeInfo.IsDebugMode && notSearchingLabel) + if (runtimeInfo.IsDebugMode && notSearchingLabel && debugEventArgs != null) { debugEventArgs.CurrentLine = runtimeInfo.CurrentLine.FromSafeString(); runtimeInfo.LineExecuted(debugEventArgs); @@ -402,9 +418,93 @@ public class YesNtInterpreter for (int i = 0; i < lines.Length; i++) { - runtimeInfo.Lines.Add(new Line(lines[i], Path.GetFileName(path), i)); + string content = lines[i].Trim().Replace("\r", string.Empty); + runtimeInfo.Lines.Add(new Line(content, Path.GetFileName(path), i)); } + PreScanLines(); return true; } + + internal void PreScanLines() + { + runtimeInfo.BlockBoundaries.Clear(); + lineMatchingHandlers = new List>(runtimeInfo.Lines.Count); + + // Dictionary to track open blocks by their expected end statement name + var openBlocks = new Dictionary>(); + + for (int i = 0; i < runtimeInfo.Lines.Count; i++) + { + string content = runtimeInfo.Lines[i].Content; + List matchingHandlers = []; + + foreach (StatementHandler handler in statementHandlers) + { + if (IsPossibleMatch(content, handler)) + { + matchingHandlers.Add(handler); + + // Track block starts (skip intermediates — they are handled separately below) + string blockPair = handler.Attribute.BlockPair; + if (!string.IsNullOrEmpty(blockPair) && !handler.Attribute.IsBlockIntermediate) + { + if (!openBlocks.TryGetValue(blockPair, out var stack)) + { + stack = new Stack(); + openBlocks[blockPair] = stack; + } + stack.Push(i); + } + + // Track block ends + if (handler.Attribute.IsBlockEnd) + { + if (openBlocks.TryGetValue(handler.Attribute.Name, out var stack) && stack.Count > 0) + { + int startLine = stack.Pop(); + runtimeInfo.BlockBoundaries[startLine] = i; + runtimeInfo.BlockBoundaries[i] = startLine; + } + } + + // Track block intermediates (e.g., else:): pop the opener, record boundary, push self + if (handler.Attribute.IsBlockIntermediate) + { + string intermediatePair = handler.Attribute.BlockPair; + if (!string.IsNullOrEmpty(intermediatePair)) + { + if (!openBlocks.TryGetValue(intermediatePair, out var stack)) + { + stack = new Stack(); + openBlocks[intermediatePair] = stack; + } + if (stack.Count > 0) + { + int startLine = stack.Pop(); + runtimeInfo.BlockBoundaries[startLine] = i; + } + stack.Push(i); + } + } + } + } + lineMatchingHandlers.Add(matchingHandlers); + } + } + + private static bool IsPossibleMatch(string content, StatementHandler handler) + { + StatementAttribute attr = handler.Attribute; + string fullName = handler.FullName; + + return attr.SearchMode switch + { + SearchMode.Exact => content == fullName, + SearchMode.StartOfLine => content.StartsWith(fullName, StringComparison.Ordinal), + SearchMode.EndOfLine => content.EndsWith(fullName, StringComparison.Ordinal), + SearchMode.Contains => content.Contains(fullName, StringComparison.Ordinal), + _ => false + }; + } } diff --git a/YesNt.Interpreter/Statements/CodeFlowStatements.cs b/YesNt.Interpreter/Statements/CodeFlowStatements.cs index ab77405..b3623c1 100644 --- a/YesNt.Interpreter/Statements/CodeFlowStatements.cs +++ b/YesNt.Interpreter/Statements/CodeFlowStatements.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using YesNt.Interpreter.Attributes; @@ -80,14 +80,7 @@ internal class CodeFlowStatements : StatementRuntimeInformation return; } - if (RuntimeInfo.Labels.ContainsKey(key)) - { - RuntimeInfo.Labels[key] = RuntimeInfo.LineNumber; - } - else - { - RuntimeInfo.Labels.Add(key, RuntimeInfo.LineNumber); - } + RuntimeInfo.Labels[key] = RuntimeInfo.LineNumber; if (!string.IsNullOrWhiteSpace(RuntimeInfo.SearchLabel) && RuntimeInfo.SearchLabel == key) { @@ -153,7 +146,7 @@ internal class CodeFlowStatements : StatementRuntimeInformation } } - [Statement("if", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow, Separator = ":")] + [Statement("if", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow, Separator = ":", BlockPair = "end_if")] public void IfBlock(string args) { args = args.Trim(); @@ -177,7 +170,7 @@ internal class CodeFlowStatements : StatementRuntimeInformation return; } - (int targetLine, _) = FindElseOrEndIf(RuntimeInfo.LineNumber); + int targetLine = FindBlockBoundary(RuntimeInfo.LineNumber); if (targetLine < 0) { RuntimeInfo.Exit(ExitMessages.NoMatchingEndIf, true); @@ -187,10 +180,10 @@ internal class CodeFlowStatements : StatementRuntimeInformation RuntimeInfo.LineNumber = targetLine; } - [Statement("else:", SearchMode.Exact, SpaceAround.None, ConsoleColor.Green)] + [Statement("else:", SearchMode.Exact, SpaceAround.None, ConsoleColor.Green, IsBlockIntermediate = true, BlockPair = "end_if")] public void Else(string _) { - int targetLine = FindEndIf(RuntimeInfo.LineNumber); + int targetLine = FindBlockBoundary(RuntimeInfo.LineNumber); if (targetLine < 0) { RuntimeInfo.Exit(ExitMessages.NoMatchingEndIf, true); @@ -200,12 +193,12 @@ internal class CodeFlowStatements : StatementRuntimeInformation RuntimeInfo.LineNumber = targetLine; } - [Statement("end_if", SearchMode.Exact, SpaceAround.None, ConsoleColor.Green)] + [Statement("end_if", SearchMode.Exact, SpaceAround.None, ConsoleColor.Green, IsBlockEnd = true)] public void EndIf(string _) { } - [Statement("while", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow, Separator = ":")] + [Statement("while", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow, Separator = ":", BlockPair = "end_while")] public void While(string args) { args = args.Trim(); @@ -229,7 +222,7 @@ internal class CodeFlowStatements : StatementRuntimeInformation return; } - int endWhileLine = FindEndWhile(RuntimeInfo.LineNumber); + int endWhileLine = FindBlockBoundary(RuntimeInfo.LineNumber); if (endWhileLine < 0) { RuntimeInfo.Exit(ExitMessages.NoMatchingEndWhile, true); @@ -239,10 +232,10 @@ internal class CodeFlowStatements : StatementRuntimeInformation RuntimeInfo.LineNumber = endWhileLine; } - [Statement("end_while", SearchMode.Exact, SpaceAround.None, ConsoleColor.Green)] + [Statement("end_while", SearchMode.Exact, SpaceAround.None, ConsoleColor.Green, IsBlockEnd = true)] public void EndWhile(string _) { - int whileLine = FindWhile(RuntimeInfo.LineNumber); + int whileLine = FindBlockBoundary(RuntimeInfo.LineNumber); if (whileLine < 0) { RuntimeInfo.Exit(ExitMessages.NoMatchingWhile, true); @@ -265,11 +258,8 @@ internal class CodeFlowStatements : StatementRuntimeInformation return; } - else - { - RuntimeInfo.IsInFunction = false; - } + RuntimeInfo.IsInFunction = false; RuntimeInfo.Exit(ExitMessages.PlannedTermination, false); } @@ -286,11 +276,8 @@ internal class CodeFlowStatements : StatementRuntimeInformation return; } - else - { - RuntimeInfo.IsInFunction = false; - } + RuntimeInfo.IsInFunction = false; RuntimeInfo.Exit(ExitMessages.PlannedTerminationCancelingTasks, true); } @@ -306,136 +293,8 @@ internal class CodeFlowStatements : StatementRuntimeInformation RuntimeInfo.Exit(message, false); } - private static string NormalizeBlockName(string value) + private int FindBlockBoundary(int currentLine) { - return value.Trim().TrimEnd(':').Trim(); - } - - private (int TargetLine, bool IsElse) FindElseOrEndIf(int currentLine) - { - int depth = 0; - - for (int i = currentLine + 1; i < RuntimeInfo.Lines.Count; i++) - { - string line = RuntimeInfo.Lines[i].Content.Trim().Replace("\r", string.Empty); - - if (IsIfStart(line)) - { - depth++; - continue; - } - - if (line == "end_if") - { - if (depth == 0) - { - return (i, false); - } - - depth--; - continue; - } - - if (line == "else:" && depth == 0) - { - return (i, true); - } - } - - return (-1, false); - } - - private int FindEndIf(int currentLine) - { - int depth = 0; - - for (int i = currentLine + 1; i < RuntimeInfo.Lines.Count; i++) - { - string line = RuntimeInfo.Lines[i].Content.Trim().Replace("\r", string.Empty); - - if (IsIfStart(line)) - { - depth++; - continue; - } - - if (line == "end_if") - { - if (depth == 0) - { - return i; - } - - depth--; - } - } - - return -1; - } - - private static bool IsIfStart(string line) - { - return line.StartsWith("if ", StringComparison.Ordinal) && line.EndsWith(':'); - } - - private int FindEndWhile(int currentLine) - { - int depth = 0; - - for (int i = currentLine + 1; i < RuntimeInfo.Lines.Count; i++) - { - string line = RuntimeInfo.Lines[i].Content.Trim().Replace("\r", string.Empty); - - if (IsWhileStart(line)) - { - depth++; - continue; - } - - if (line == "end_while") - { - if (depth == 0) - { - return i; - } - - depth--; - } - } - - return -1; - } - - private int FindWhile(int currentLine) - { - int depth = 0; - - for (int i = currentLine - 1; i >= 0; i--) - { - string line = RuntimeInfo.Lines[i].Content.Trim().Replace("\r", string.Empty); - - if (line == "end_while") - { - depth++; - continue; - } - - if (IsWhileStart(line)) - { - if (depth == 0) - { - return i; - } - - depth--; - } - } - - return -1; - } - - private static bool IsWhileStart(string line) - { - return line.StartsWith("while ", StringComparison.Ordinal) && line.EndsWith(':'); + return RuntimeInfo.BlockBoundaries.TryGetValue(currentLine, out int cached) ? cached : -1; } } diff --git a/YesNt.Interpreter/Statements/FunctionStatements.cs b/YesNt.Interpreter/Statements/FunctionStatements.cs index 3c60faf..fb83927 100644 --- a/YesNt.Interpreter/Statements/FunctionStatements.cs +++ b/YesNt.Interpreter/Statements/FunctionStatements.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using YesNt.Interpreter.Attributes; @@ -33,14 +33,7 @@ internal class FunctionStatements : StatementRuntimeInformation return; } - if (RuntimeInfo.Functions.ContainsKey(key)) - { - RuntimeInfo.Functions[key] = RuntimeInfo.LineNumber; - } - else - { - RuntimeInfo.Functions.Add(key, RuntimeInfo.LineNumber); - } + RuntimeInfo.Functions[key] = RuntimeInfo.LineNumber; if (!string.IsNullOrWhiteSpace(RuntimeInfo.SearchFunction) && RuntimeInfo.SearchFunction == key) { @@ -59,18 +52,7 @@ internal class FunctionStatements : StatementRuntimeInformation [Statement("%out", SearchMode.Contains, SpaceAround.None, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)] public void GetOutParameter(string args) { - while (args.Contains("%out")) - { - if (RuntimeInfo.OutParametersStack.Count == 0) - { - RuntimeInfo.Exit(ExitMessages.NoOutArgumentInStack, true); - return; - } - - args = args.ReplaceFirstOccurrence("%out", RuntimeInfo.OutParametersStack.Pop()); - } - - RuntimeInfo.CurrentLine = args.TrimEnd(); + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessStackParameters(args, "%out", RuntimeInfo.OutParametersStack, RuntimeInfo, ExitMessages.NoOutArgumentInStack); } [Statement("%has_out", SearchMode.Contains, SpaceAround.None, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)] @@ -122,18 +104,7 @@ internal class FunctionStatements : StatementRuntimeInformation return; } - while (args.Contains("%in")) - { - if (RuntimeInfo.FunctionCallStack.Peek().Arguments.Count == 0) - { - RuntimeInfo.Exit(ExitMessages.NoInArgumentInStack, true); - return; - } - - args = args.ReplaceFirstOccurrence("%in", RuntimeInfo.FunctionCallStack.Peek().Arguments.Pop()); - } - - RuntimeInfo.CurrentLine = args.TrimEnd(); + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessStackParameters(args, "%in", RuntimeInfo.FunctionCallStack.Peek().Arguments, RuntimeInfo, ExitMessages.NoInArgumentInStack); } [Statement("%has_in", SearchMode.Contains, SpaceAround.None, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)] @@ -181,10 +152,8 @@ internal class FunctionStatements : StatementRuntimeInformation } return; } - else - { - RuntimeInfo.IsInFunction = false; - } + + RuntimeInfo.IsInFunction = false; if (RuntimeInfo.FunctionCallStack.Count > 0) { @@ -204,9 +173,4 @@ internal class FunctionStatements : StatementRuntimeInformation { RuntimeInfo.FunctionCallStack.Clear(); } - - private static string NormalizeBlockName(string value) - { - return value.Trim().TrimEnd(':').Trim(); - } } diff --git a/YesNt.Interpreter/Statements/PredefinedVariableStatements.cs b/YesNt.Interpreter/Statements/PredefinedVariableStatements.cs index 4036f94..c1c1c11 100644 --- a/YesNt.Interpreter/Statements/PredefinedVariableStatements.cs +++ b/YesNt.Interpreter/Statements/PredefinedVariableStatements.cs @@ -1,4 +1,5 @@ -using System; +using System; +using System.Runtime.InteropServices; using YesNt.Interpreter.Attributes; using YesNt.Interpreter.Enums; @@ -14,51 +15,36 @@ internal class PredefinedVariableStatements : StatementRuntimeInformation [Statement("%time", SearchMode.Contains, SpaceAround.None, ConsoleColor.Blue, KeepStatementInArgs = true, Priority = Priority.Highest)] public void GetUnixTimestamp(string args) { - args = args.Replace("%time", DateTimeOffset.Now.ToUnixTimeSeconds().ToString()); - - RuntimeInfo.CurrentLine = args.TrimEnd(); + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessSimplePlaceholders(args, "%time", DateTimeOffset.Now.ToUnixTimeSeconds().ToString()).TrimEnd(); } [Statement("%os", SearchMode.Contains, SpaceAround.None, ConsoleColor.Blue, KeepStatementInArgs = true, Priority = Priority.Highest)] public void GetOperatingSystem(string args) { - args = args.Replace("%os", Environment.OSVersion.Platform.ToString()); - - RuntimeInfo.CurrentLine = args.TrimEnd(); + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessSimplePlaceholders(args, "%os", Environment.OSVersion.Platform.ToString()).TrimEnd(); } [Statement("%cpu", SearchMode.Contains, SpaceAround.None, ConsoleColor.Blue, KeepStatementInArgs = true, Priority = Priority.Highest)] public void GetProcessorArchitecture(string args) { - args = args.Replace("%cpu", System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture.ToString()); - - RuntimeInfo.CurrentLine = args.TrimEnd(); + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessSimplePlaceholders(args, "%cpu", System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture.ToString()).TrimEnd(); } [Statement("%is64", SearchMode.Contains, SpaceAround.None, ConsoleColor.Blue, KeepStatementInArgs = true, Priority = Priority.Highest)] public void GetIsOperatingSystem64Bit(string args) { - args = args.Replace("%is64", $"{Environment.Is64BitOperatingSystem}"); - - RuntimeInfo.CurrentLine = args.TrimEnd(); + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessSimplePlaceholders(args, "%is64", Environment.Is64BitOperatingSystem.ToString()).TrimEnd(); } [Statement("%pi", SearchMode.Contains, SpaceAround.None, ConsoleColor.Blue, KeepStatementInArgs = true, Priority = Priority.Highest)] public void GetPi(string args) { - args = args.Replace("%pi", Math.PI.ToString()); - - RuntimeInfo.CurrentLine = args.TrimEnd(); + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessSimplePlaceholders(args, "%pi", Math.PI.ToString()).TrimEnd(); } [Statement("%rand", SearchMode.Contains, SpaceAround.None, ConsoleColor.Blue, KeepStatementInArgs = true, Priority = Priority.Highest)] public void GetRandom(string args) { - while (args.Contains("%rand")) - { - args = args.ReplaceFirstOccurrence("%rand", random.Next(32767, int.MaxValue).ToString()); - } - - RuntimeInfo.CurrentLine = args.TrimEnd(); + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessDynamicPlaceholders(args, "%rand", () => random.Next(32767, int.MaxValue).ToString()).TrimEnd(); } } diff --git a/YesNt.Interpreter/Statements/ProcessingStatements.cs b/YesNt.Interpreter/Statements/ProcessingStatements.cs index 010389d..d1c4983 100644 --- a/YesNt.Interpreter/Statements/ProcessingStatements.cs +++ b/YesNt.Interpreter/Statements/ProcessingStatements.cs @@ -16,20 +16,7 @@ internal partial class ProcessingStatements : StatementRuntimeInformation [Statement("calc", SearchMode.EndOfLine, SpaceAround.Start, ConsoleColor.DarkYellow, Priority = Priority.High)] public void Calculate(string args) { - MatchCollection matches = CalculationRegex().Matches(args.FromSafeString()); - - for (int i = 0; i < matches.Count; i++) - { - string res = Evaluator.Calculate(matches[i].Value); - if (res is null) - { - RuntimeInfo.Exit(ExitMessages.InvalidOperation, true); - return; - } - args = args.FromSafeString().Replace(matches[i].Value, res); - } - - RuntimeInfo.CurrentLine = args; + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessCalculations(args.FromSafeString(), RuntimeInfo, CalculationRegex()); } [Statement("eval", SearchMode.EndOfLine, SpaceAround.Start, ConsoleColor.DarkYellow, Priority = Priority.VeryHigh)] @@ -100,6 +87,7 @@ internal partial class ProcessingStatements : StatementRuntimeInformation { RuntimeInfo.Lines.Insert(RuntimeInfo.LineNumber + i, new Line(lines[i], Path.GetFileName(path), i)); } + RuntimeInfo.PreScanLinesAction?.Invoke(); RuntimeInfo.LineNumber--; } catch diff --git a/YesNt.Interpreter/Statements/VariableStatements.cs b/YesNt.Interpreter/Statements/VariableStatements.cs index f07f640..a690b27 100644 --- a/YesNt.Interpreter/Statements/VariableStatements.cs +++ b/YesNt.Interpreter/Statements/VariableStatements.cs @@ -1,8 +1,10 @@ -using System.Text.RegularExpressions; +using System; +using System.Collections.Generic; using YesNt.Interpreter.Attributes; using YesNt.Interpreter.Enums; using YesNt.Interpreter.Runtime; +using YesNt.Interpreter.Utilities; namespace YesNt.Interpreter.Statements; @@ -11,32 +13,16 @@ internal partial class VariableStatements : StatementRuntimeInformation [Statement("var", SearchMode.StartOfLine, SpaceAround.End, System.ConsoleColor.DarkBlue, Priority = Priority.VeryLow, Separator = "=")] public void DefineVariable(string args) { - string[] parts = args.Split('='); - if (parts.Length == 2) - { - string key = parts[0].Trim(); - if (key.Contains(' ')) - { - RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true); - } - - if (RuntimeInfo.Variables.ContainsKey(key)) - { - RuntimeInfo.Variables[key] = parts[1].Trim(); - } - else - { - RuntimeInfo.Variables.Add(key, parts[1].Trim()); - } - } - else - { - RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true); - } + DefineVariableIn(RuntimeInfo.Variables, args); } [Statement("global", SearchMode.StartOfLine, SpaceAround.End, System.ConsoleColor.DarkBlue, Priority = Priority.VeryLow, Separator = "=")] public void DefineGlobalVariable(string args) + { + DefineVariableIn(RuntimeInfo.GlobalVariables, args); + } + + private void DefineVariableIn(Dictionary dict, string args) { string[] parts = args.Split('='); if (parts.Length == 2) @@ -47,14 +33,7 @@ internal partial class VariableStatements : StatementRuntimeInformation RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true); } - if (RuntimeInfo.GlobalVariables.ContainsKey(key)) - { - RuntimeInfo.GlobalVariables[key] = parts[1].Trim(); - } - else - { - RuntimeInfo.GlobalVariables.Add(key, parts[1].Trim()); - } + dict[key] = parts[1].Trim(); } else { @@ -84,38 +63,6 @@ internal partial class VariableStatements : StatementRuntimeInformation [Statement("${", SearchMode.Contains, SpaceAround.None, Priority = Priority.Highest, Separator = "}")] public void ReadVariable(string _) { - if (!RuntimeInfo.CurrentLine.Contains("${")) - { - return; - } - - MatchCollection matches = VariableStatementRegex().Matches(RuntimeInfo.CurrentLine); - - if (matches.Count <= 0) - { - return; - } - - for (int i = 0; i < matches.Count; i++) - { - string varName = matches[i].Groups[1].Value; - if (RuntimeInfo.Variables.TryGetValue(varName, out string value)) - { - RuntimeInfo.CurrentLine = RuntimeInfo.CurrentLine.Replace(matches[i].Value, value); - } - else if (RuntimeInfo.GlobalVariables.TryGetValue(varName, out value)) - { - RuntimeInfo.CurrentLine = RuntimeInfo.CurrentLine.Replace(matches[i].Value, value); - } - else if (!RuntimeInfo.IsSearching) - { - RuntimeInfo.Exit(ExitMessages.VariableNotFound(varName), true); - return; - } - } + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessVariables(RuntimeInfo.CurrentLine, RuntimeInfo); } - - [GeneratedRegex("\\$\\{([a-zA-Z0-9]+)\\}")] - private static partial Regex VariableStatementRegex(); } - diff --git a/YesNt.Interpreter/Utilities/Evaluator.cs b/YesNt.Interpreter/Utilities/Evaluator.cs index 2aad718..a3baba1 100644 --- a/YesNt.Interpreter/Utilities/Evaluator.cs +++ b/YesNt.Interpreter/Utilities/Evaluator.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Linq; using System.Text.RegularExpressions; @@ -19,11 +19,13 @@ internal static partial class Evaluator /// public static bool? EvaluateCondition(string input) { - if (input.ToLower().FromSafeString().Trim() == "true") + input = input.FromSafeString(); + string lower = input.ToLower().Trim(); + if (lower == "true") { return true; } - else if (input.ToLower().FromSafeString().Trim() == "false") + else if (lower == "false") { return false; } @@ -31,16 +33,16 @@ internal static partial class Evaluator string[] parts = input.Split("=="); if (parts.Length == 2) { - string part1 = parts[0].FromSafeString().Trim(); - string part2 = parts[1].FromSafeString().Trim(); + string part1 = parts[0].Trim(); + string part2 = parts[1].Trim(); return part1 == part2; } parts = input.Split("!="); if (parts.Length == 2) { - string part1 = parts[0].FromSafeString().Trim(); - string part2 = parts[1].FromSafeString().Trim(); + string part1 = parts[0].Trim(); + string part2 = parts[1].Trim(); return part1 != part2; } @@ -89,23 +91,26 @@ internal static partial class Evaluator /// The result as a culture-invariant numeric string, or "NaN" if evaluation failed. public static string Calculate(string input) { + input = input.FromSafeString(); input = PlusPlusRegex().Replace(input, "+"); input = MinusMinusRegex().Replace(input, "+"); input = MinusPlusRegex().Replace(input, "-"); input = PlusMinusRegex().Replace(input, "-"); - string yes = Calculate(input, '+'); - return yes; + return CalculateInternal(input, '+'); } - private static string Calculate(string input, char op) + private static string CalculateInternal(string input, char op) { - if (input is null) + if (string.IsNullOrWhiteSpace(input)) { return null; } - input = input.FromSafeString(); + if (input.ToStandardizedNumber(out double quickNum)) + { + return quickNum.ToString(System.Globalization.CultureInfo.InvariantCulture); + } MatchCollection matches = ParenthesesRegex().Matches(input); while (matches.Count > 0) @@ -113,7 +118,7 @@ internal static partial class Evaluator for (int i = 0; i < matches.Count; i++) { string calc = matches[i].Value.Substring(1, matches[i].Length - 2); - string ret = Calculate(calc); + string ret = CalculateInternal(calc, '+'); input = input.Replace(matches[i].Value, ret); } matches = ParenthesesRegex().Matches(input); @@ -136,11 +141,11 @@ internal static partial class Evaluator part = op switch { - '+' => Calculate(part, '-'), - '-' => Calculate(part, '*'), - '*' => Calculate(part, '/'), - '/' => Calculate(part, '%'), - '%' => Calculate(part, '^'), + '+' => CalculateInternal(part, '-'), + '-' => CalculateInternal(part, '*'), + '*' => CalculateInternal(part, '/'), + '/' => CalculateInternal(part, '%'), + '%' => CalculateInternal(part, '^'), _ => part }; @@ -208,4 +213,4 @@ internal static partial class Evaluator [GeneratedRegex("(\\+ +\\-)+")] private static partial Regex PlusMinusRegex(); -} \ No newline at end of file +} diff --git a/YesNt.Interpreter/Utilities/FixedProcess.cs b/YesNt.Interpreter/Utilities/FixedProcess.cs index 9430c88..1d2cdc2 100644 --- a/YesNt.Interpreter/Utilities/FixedProcess.cs +++ b/YesNt.Interpreter/Utilities/FixedProcess.cs @@ -19,7 +19,7 @@ internal delegate void UserCallBack(string data); /// flushes whatever is in the read buffer immediately, enabling real-time /// output forwarding for interactive child processes. /// -public class FixedProcess : Process +internal class FixedProcess : Process { public new event DataReceivedEventHandler OutputDataReceived; diff --git a/YesNt.Interpreter/Utilities/StringExtensions.cs b/YesNt.Interpreter/Utilities/StringExtensions.cs index b5ff31b..fb665c7 100644 --- a/YesNt.Interpreter/Utilities/StringExtensions.cs +++ b/YesNt.Interpreter/Utilities/StringExtensions.cs @@ -64,13 +64,30 @@ public static class StringExtensions /// The safe-string encoded representation. public static string ToSafeString(this string input) { - StringBuilder output = new StringBuilder(); - foreach (char c in input) + if (string.IsNullOrEmpty(input)) { - _ = output.Append($"\v{c}\v"); + return input; } - return ReplaceOnce(output.ToString(), ReplacementRules); + StringBuilder output = new StringBuilder(input.Length * 3); + foreach (char c in input) + { + string s = c.ToString(); + if (ReplacementRules.TryGetValue(s, out string replacement)) + { + _ = output.Append('\v'); + _ = output.Append(replacement); + _ = output.Append('\v'); + } + else + { + _ = output.Append('\v'); + _ = output.Append(c); + _ = output.Append('\v'); + } + } + + return output.ToString(); } /// @@ -80,7 +97,35 @@ public static class StringExtensions /// The decoded plain string. public static string FromSafeString(this string input) { - return ReplaceOnce(input.Replace("\v", string.Empty), reverseReplacementRules); + if (string.IsNullOrEmpty(input) || (!input.Contains('\v') && !input.Contains('\x01'))) + { + return input; + } + + string stripped = input.Replace("\v", string.Empty); + if (!stripped.Contains('\x01')) + { + return stripped; + } + + StringBuilder output = new StringBuilder(stripped.Length); + for (int i = 0; i < stripped.Length; i++) + { + if (stripped[i] == '\x01' && i + 4 < stripped.Length && stripped[i + 4] == '\x01') + { + string code = stripped.Substring(i, 5); + if (reverseReplacementRules.TryGetValue(code, out string value)) + { + _ = output.Append(value); + i += 4; + continue; + } + } + + _ = output.Append(stripped[i]); + } + + return output.ToString(); } /// @@ -92,7 +137,11 @@ public static class StringExtensions /// if parsing succeeded; otherwise . public static bool ToStandardizedNumber(this string input, out double result) { - return double.TryParse(input.FromSafeString().Replace(',', '.'), NumberStyles.Any, CultureInfo.InvariantCulture, out result); + if (input.IndexOf(',') != -1) + { + input = input.Replace(',', '.'); + } + return double.TryParse(input, NumberStyles.Any, CultureInfo.InvariantCulture, out result); } /// Replaces only the first occurrence of in the string. @@ -131,24 +180,4 @@ public static class StringExtensions return count; } - - private static string ReplaceOnce(string input, Dictionary replacementRules) - { - // \x01emp\x01/string.Empty is a special case, it is used to represent empty strings and won't work with the normal rules because an empty string always matches and causes an infinite loop 3 letter abbreviation - IEnumerable> matches = replacementRules.Where(rule => rule.Key != string.Empty && input.Contains(rule.Key, StringComparison.Ordinal)); - if (!matches.Any()) - { - return input; - } - - KeyValuePair match = matches.First(); - int startIndex = input.IndexOf(match.Key, StringComparison.Ordinal); - int endIndex = startIndex + match.Key.Length; - - string before = ReplaceOnce(input[..startIndex], replacementRules); - string replaced = match.Value; - string after = ReplaceOnce(input[endIndex..], replacementRules); - - return before + replaced + after; - } } \ No newline at end of file diff --git a/YesNt.Interpreter/Utilities/TemplateProcessor.cs b/YesNt.Interpreter/Utilities/TemplateProcessor.cs new file mode 100644 index 0000000..368a171 --- /dev/null +++ b/YesNt.Interpreter/Utilities/TemplateProcessor.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using YesNt.Interpreter.Runtime; + +namespace YesNt.Interpreter.Utilities; + +/// +/// Provides high-performance template substitution for variables and stack parameters. +/// +internal static class TemplateProcessor +{ + /// + /// Replaces all occurrences of ${variableName} with their current values. + /// + public static string ProcessVariables(string input, RuntimeInformation runtimeInfo) + { + if (string.IsNullOrEmpty(input)) return input; + + int startIdx = input.IndexOf("${", StringComparison.Ordinal); + if (startIdx == -1) return input; + + StringBuilder sb = new StringBuilder(input.Length); + int lastIdx = 0; + + while (startIdx != -1) + { + sb.Append(input, lastIdx, startIdx - lastIdx); + int endIdx = input.IndexOf('}', startIdx + 2); + if (endIdx == -1) + { + sb.Append("${"); + lastIdx = startIdx + 2; + } + else + { + string varName = input.Substring(startIdx + 2, endIdx - (startIdx + 2)); + if (runtimeInfo.Variables.TryGetValue(varName, out string value)) + { + sb.Append(value); + } + else if (runtimeInfo.GlobalVariables.TryGetValue(varName, out value)) + { + sb.Append(value); + } + else if (!runtimeInfo.IsSearching) + { + runtimeInfo.Exit(ExitMessages.VariableNotFound(varName), true); + return input; + } + else + { + sb.Append("${"); + sb.Append(varName); + sb.Append('}'); + } + + lastIdx = endIdx + 1; + } + + startIdx = input.IndexOf("${", lastIdx, StringComparison.Ordinal); + } + + sb.Append(input, lastIdx, input.Length - lastIdx); + return sb.ToString(); + } + + /// + /// 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) + { + if (string.IsNullOrEmpty(input)) return input; + + int startIdx = input.IndexOf(placeholder, StringComparison.Ordinal); + if (startIdx == -1) return input; + + StringBuilder sb = new StringBuilder(input.Length); + int lastIdx = 0; + int placeholderLen = placeholder.Length; + + while (startIdx != -1) + { + sb.Append(input, lastIdx, startIdx - lastIdx); + if (stack.Count == 0) + { + runtimeInfo.Exit(emptyStackMessage, true); + return input; + } + sb.Append(stack.Pop()); + lastIdx = startIdx + placeholderLen; + startIdx = input.IndexOf(placeholder, lastIdx, StringComparison.Ordinal); + } + + sb.Append(input, lastIdx, input.Length - lastIdx); + return sb.ToString(); + } + + /// + /// Replaces all occurrences of a placeholder with a fixed value. + /// + public static string ProcessSimplePlaceholders(string input, string placeholder, string value) + { + if (string.IsNullOrEmpty(input)) return input; + + return input.Replace(placeholder, value, StringComparison.Ordinal); + } + + /// + /// Replaces all occurrences of a placeholder with values generated by a provider function. + /// + public static string ProcessDynamicPlaceholders(string input, string placeholder, Func valueProvider) + { + if (string.IsNullOrEmpty(input)) return input; + + int startIdx = input.IndexOf(placeholder, StringComparison.Ordinal); + if (startIdx == -1) return input; + + StringBuilder sb = new StringBuilder(input.Length); + int lastIdx = 0; + int placeholderLen = placeholder.Length; + + while (startIdx != -1) + { + sb.Append(input, lastIdx, startIdx - lastIdx); + sb.Append(valueProvider()); + lastIdx = startIdx + placeholderLen; + startIdx = input.IndexOf(placeholder, lastIdx, StringComparison.Ordinal); + } + + sb.Append(input, lastIdx, input.Length - lastIdx); + return sb.ToString(); + } + + /// + /// Replaces all occurrences of arithmetic expressions with their results. + /// + public static string ProcessCalculations(string input, RuntimeInformation runtimeInfo, Regex calculationRegex) + { + if (string.IsNullOrEmpty(input)) return input; + + MatchCollection matches = calculationRegex.Matches(input); + if (matches.Count == 0) return input; + + StringBuilder sb = new StringBuilder(input.Length); + int lastIdx = 0; + + for (int i = 0; i < matches.Count; i++) + { + Match match = matches[i]; + sb.Append(input, lastIdx, match.Index - lastIdx); + + string res = Evaluator.Calculate(match.Value); + if (res is null) + { + runtimeInfo.Exit(ExitMessages.InvalidOperation, true); + return input; + } + sb.Append(res); + lastIdx = match.Index + match.Length; + } + + sb.Append(input, lastIdx, input.Length - lastIdx); + return sb.ToString(); + } +}