Performance improvements and code cleanup

This commit is contained in:
Stone_Red
2026-03-05 22:39:05 +01:00
parent 4bcaf30d9e
commit 13ef994d8b
15 changed files with 488 additions and 404 deletions
@@ -1,4 +1,4 @@
using System;
using System;
using YesNt.Interpreter.Enums;
@@ -60,6 +60,24 @@ public class StatementAttribute : Attribute
/// </summary>
public string Separator { get; set; }
/// <summary>
/// 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").
/// </summary>
public string BlockPair { get; set; }
/// <summary>
/// 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).
/// </summary>
public bool IsBlockEnd { get; set; }
/// <summary>
/// Gets or sets a value indicating whether this statement is an intermediate part of a block
/// (e.g., "else:" between "if" and "end_if").
/// </summary>
public bool IsBlockIntermediate { get; set; }
/// <summary>
/// Initializes a new <see cref="StatementAttribute"/> with a syntax-highlight color.
/// </summary>
+3 -1
View File
@@ -1,4 +1,6 @@
namespace YesNt.Interpreter.Runtime;
using System.Collections.Generic;
namespace YesNt.Interpreter.Runtime;
/// <summary>
/// Represents a single source line together with its location metadata.
@@ -26,6 +26,8 @@ internal sealed class RuntimeInformation
public Dictionary<string, string> GlobalVariables { get; set; } = [];
public Dictionary<string, int> Functions { get; } = [];
public Dictionary<int, int> BlockBoundaries { get; } = [];
internal Action PreScanLinesAction { get; set; }
public Stack<FunctionScope> FunctionCallStack { get; } = new();
public Stack<string> InParametersStack { get; } = new();
public Stack<string> 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)
@@ -0,0 +1,9 @@
using System;
using YesNt.Interpreter.Attributes;
namespace YesNt.Interpreter.Runtime;
/// <summary>
/// Pre-calculated statement handler information for faster matching.
/// </summary>
internal record StatementHandler(StatementAttribute Attribute, Action<string> Handler, string FullName);
@@ -13,4 +13,12 @@ internal abstract class StatementRuntimeInformation
/// Injected by the generated registry before any handler is invoked.
/// </summary>
public RuntimeInformation RuntimeInfo { get; set; }
/// <summary>
/// Trims surrounding whitespace and a trailing colon from a block or function name.
/// </summary>
protected static string NormalizeBlockName(string value)
{
return value.Trim().TrimEnd(':').Trim();
}
}
+155 -55
View File
@@ -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;
/// <summary>
/// The main entry point for executing YesNt scripts.
/// </summary>
/// <example>
/// Running a script file:
/// <code>
/// var interpreter = new YesNtInterpreter();
/// interpreter.Execute("path/to/script.ynt");
/// </code>
/// Running script lines in memory with a custom statement:
/// <code>
/// var interpreter = new YesNtInterpreter();
/// interpreter.AddStatement("log", SearchMode.StartOfLine, SpaceAround.End, args =>
/// Console.WriteLine($"[LOG] {args}"));
/// interpreter.Execute(new List&lt;string&gt; { "log hello world" });
/// </code>
/// </example>
public class YesNtInterpreter
{
/// <summary>
@@ -43,6 +29,8 @@ public class YesNtInterpreter
private readonly RuntimeInformation runtimeInfo = new RuntimeInformation();
private Dictionary<StatementAttribute, Action<string>> statements;
private List<StatementHandler> statementHandlers;
private List<List<StatementHandler>> lineMatchingHandlers = [];
private readonly List<KeyValuePair<StaticStatementAttribute, Action>> staticStatements;
private readonly Dictionary<string, List<KeyValuePair<StatementAttribute, Action<string>>>> 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();
}
/// <summary>
/// Registers a custom statement using a pre-built <see cref="StatementAttribute"/>.
/// 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();
}
/// <summary>
/// Registers a custom statement without a syntax-highlight color.
/// Registers a simple custom statement with default settings.
/// </summary>
/// <param name="name">The keyword that identifies this statement in source code.</param>
/// <param name="searchMode">Where in the line the keyword is matched.</param>
/// <param name="spaceAround">Which sides of the keyword require a surrounding space.</param>
/// <param name="name">The keyword to match.</param>
/// <param name="searchMode">Where in the line the keyword is searched for.</param>
/// <param name="spaceAround">Which sides of the keyword must be padded with a space.</param>
/// <param name="handler">The delegate invoked when the statement matches.</param>
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action<string> handler)
{
@@ -117,12 +124,12 @@ public class YesNtInterpreter
}
/// <summary>
/// Registers a custom statement with a syntax-highlight color.
/// Registers a simple custom statement with a specific syntax-highlight color.
/// </summary>
/// <param name="name">The keyword that identifies this statement in source code.</param>
/// <param name="searchMode">Where in the line the keyword is matched.</param>
/// <param name="spaceAround">Which sides of the keyword require a surrounding space.</param>
/// <param name="consoleColor">The color used for syntax highlighting in the code editor.</param>
/// <param name="name">The keyword to match.</param>
/// <param name="searchMode">Where in the line the keyword is searched for.</param>
/// <param name="spaceAround">Which sides of the keyword must be padded with a space.</param>
/// <param name="consoleColor">The color used for syntax highlighting.</param>
/// <param name="handler">The delegate invoked when the statement matches.</param>
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor consoleColor, Action<string> handler)
{
@@ -130,10 +137,9 @@ public class YesNtInterpreter
}
/// <summary>
/// Permanently removes all built-in or custom statements that match <paramref name="name"/>.
/// After removal, any script line that would have matched triggers an "Invalid statement" error.
/// Unregisters all handlers matching the specified keyword <paramref name="name"/>.
/// </summary>
/// <param name="name">The keyword of the statement(s) to remove.</param>
/// <param name="name">The keyword to remove.</param>
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();
}
/// <summary>
@@ -171,6 +179,8 @@ public class YesNtInterpreter
{
statements[kv.Key] = _ => { };
}
UpdateStatementHandlers();
PreScanLines();
}
/// <summary>
@@ -192,6 +202,8 @@ public class YesNtInterpreter
}
_ = disabledStatements.Remove(name);
UpdateStatementHandlers();
PreScanLines();
}
/// <summary>
@@ -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<StaticStatementAttribute, Action> staticStatement in staticStatements)
{
@@ -296,9 +316,11 @@ public class YesNtInterpreter
bool statementFound = false;
bool notSearchingLabel = !runtimeInfo.IsSearching;
foreach (KeyValuePair<StatementAttribute, Action<string>> statement in statements)
List<StatementHandler> 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<List<StatementHandler>>(runtimeInfo.Lines.Count);
// Dictionary to track open blocks by their expected end statement name
var openBlocks = new Dictionary<string, Stack<int>>();
for (int i = 0; i < runtimeInfo.Lines.Count; i++)
{
string content = runtimeInfo.Lines[i].Content;
List<StatementHandler> 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<int>();
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<int>();
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
};
}
}
@@ -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;
}
}
@@ -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();
}
}
@@ -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();
}
}
@@ -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
@@ -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<string, string> 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();
}
+23 -18
View File
@@ -1,4 +1,4 @@
using System;
using System;
using System.Linq;
using System.Text.RegularExpressions;
@@ -19,11 +19,13 @@ internal static partial class Evaluator
/// </returns>
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
/// <returns>The result as a culture-invariant numeric string, or <c>"NaN"</c> if evaluation failed.</returns>
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
};
+1 -1
View File
@@ -19,7 +19,7 @@ internal delegate void UserCallBack(string data);
/// <see cref="FixedProcess"/> flushes whatever is in the read buffer immediately, enabling real-time
/// output forwarding for interactive child processes.
/// </summary>
public class FixedProcess : Process
internal class FixedProcess : Process
{
public new event DataReceivedEventHandler OutputDataReceived;
+55 -26
View File
@@ -64,13 +64,30 @@ public static class StringExtensions
/// <returns>The safe-string encoded representation.</returns>
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();
}
/// <summary>
@@ -80,7 +97,35 @@ public static class StringExtensions
/// <returns>The decoded plain string.</returns>
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();
}
/// <summary>
@@ -92,7 +137,11 @@ public static class StringExtensions
/// <returns><see langword="true"/> if parsing succeeded; otherwise <see langword="false"/>.</returns>
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);
}
/// <summary>Replaces only the first occurrence of <paramref name="oldValue"/> in the string.</summary>
@@ -131,24 +180,4 @@ public static class StringExtensions
return count;
}
private static string ReplaceOnce(string input, Dictionary<string, string> 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<KeyValuePair<string, string>> matches = replacementRules.Where(rule => rule.Key != string.Empty && input.Contains(rule.Key, StringComparison.Ordinal));
if (!matches.Any())
{
return input;
}
KeyValuePair<string, string> 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;
}
}
@@ -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;
/// <summary>
/// Provides high-performance template substitution for variables and stack parameters.
/// </summary>
internal static class TemplateProcessor
{
/// <summary>
/// Replaces all occurrences of ${variableName} with their current values.
/// </summary>
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();
}
/// <summary>
/// Replaces all occurrences of a placeholder (e.g., %in, %out) with values popped from a stack.
/// </summary>
public static string ProcessStackParameters(string input, string placeholder, Stack<string> 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();
}
/// <summary>
/// Replaces all occurrences of a placeholder with a fixed value.
/// </summary>
public static string ProcessSimplePlaceholders(string input, string placeholder, string value)
{
if (string.IsNullOrEmpty(input)) return input;
return input.Replace(placeholder, value, StringComparison.Ordinal);
}
/// <summary>
/// Replaces all occurrences of a placeholder with values generated by a provider function.
/// </summary>
public static string ProcessDynamicPlaceholders(string input, string placeholder, Func<string> 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();
}
/// <summary>
/// Replaces all occurrences of arithmetic expressions with their results.
/// </summary>
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();
}
}