Add snapcraft & chocolatey manifests and move code to src

This commit is contained in:
Stone_Red
2026-03-06 00:51:31 +01:00
parent 5efd698772
commit 2a03352169
64 changed files with 302 additions and 5 deletions
@@ -0,0 +1,110 @@
using System;
using YesNt.Interpreter.Enums;
namespace YesNt.Interpreter.Attributes;
/// <summary>
/// Marks a method as a YesNt statement handler.
/// The interpreter matches source lines against the <see cref="Name"/> keyword according to
/// <see cref="SearchMode"/> and <see cref="SpaceAround"/> rules, then invokes the decorated method
/// with the remaining argument text.
/// </summary>
/// <remarks>
/// Methods decorated with this attribute must be instance methods on a class that inherits
/// <see cref="Runtime.StatementRuntimeInformation"/> and must accept a single <see cref="string"/> parameter.
/// </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class StatementAttribute : Attribute
{
/// <summary>Gets the keyword that identifies this statement in source code.</summary>
public string Name { get; }
/// <summary>Gets where in the line the keyword is searched for.</summary>
public SearchMode SearchMode { get; }
/// <summary>Gets which sides of the keyword must be padded with a space.</summary>
public SpaceAround SpaceAround { get; }
/// <summary>Gets or sets the syntax-highlight color used by the code editor.</summary>
public ConsoleColor Color { get; set; }
/// <summary>
/// Gets or sets the execution priority. Statements with a lower <see cref="Priority"/> value
/// run before those with a higher value. Defaults to <see cref="Priority.Normal"/>.
/// </summary>
public Priority Priority { get; set; } = Priority.Normal;
/// <summary>
/// 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 <see langword="false"/>.
/// </summary>
public bool ExecuteInSearchMode { get; set; }
/// <summary>
/// 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 <see langword="false"/>.
/// </summary>
public bool KeepStatementInArgs { get; set; }
/// <summary>
/// Gets a value indicating whether this statement should be excluded from syntax highlighting.
/// Set to <see langword="true"/> when no <see cref="Color"/> is provided.
/// </summary>
public bool IgnoreSyntaxHighlighting { get; }
/// <summary>
/// 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. <c>call</c> vs <c>call … with …</c>).
/// </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>
/// <param name="name">The keyword that identifies this statement.</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="color">The color used for syntax highlighting in the code editor.</param>
public StatementAttribute(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor color)
{
Name = name;
SearchMode = searchMode;
SpaceAround = spaceAround;
Color = color;
}
/// <summary>
/// Initializes a new <see cref="StatementAttribute"/> without a syntax-highlight color.
/// The statement will be excluded from syntax highlighting.
/// </summary>
/// <param name="name">The keyword that identifies this statement.</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>
public StatementAttribute(string name, SearchMode searchMode, SpaceAround spaceAround)
{
Name = name;
SearchMode = searchMode;
SpaceAround = spaceAround;
IgnoreSyntaxHighlighting = true;
}
}
@@ -0,0 +1,31 @@
using System;
using YesNt.Interpreter.Enums;
namespace YesNt.Interpreter.Attributes;
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// Methods decorated with this attribute must be instance methods on a class that inherits
/// <see cref="Runtime.StatementRuntimeInformation"/> and must have no parameters.
/// </remarks>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class StaticStatementAttribute : Attribute
{
/// <summary>
/// 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 <see langword="false"/>.
/// </summary>
public bool ExecuteInSearchMode { get; set; }
/// <summary>
/// Gets or sets the execution priority relative to other static statements.
/// Defaults to <see cref="Priority.Normal"/>.
/// </summary>
public Priority Priority { get; set; } = Priority.Normal;
}
+28
View File
@@ -0,0 +1,28 @@
namespace YesNt.Interpreter.Enums;
/// <summary>
/// Controls the execution order of statements. Lower values run first.
/// </summary>
public enum Priority
{
/// <summary>Runs before all other statements. Used for syntax pre-processing such as string literals.</summary>
PreProcessing,
/// <summary>Runs very early. Used for inline substitutions such as variable reads and parameter pops.</summary>
Highest,
/// <summary>Runs early.</summary>
VeryHigh,
/// <summary>Runs above normal order.</summary>
High,
/// <summary>Default execution order.</summary>
Normal,
/// <summary>Runs below normal order.</summary>
Low,
/// <summary>Runs last. Used for control-flow and variable definitions that depend on substitutions being complete.</summary>
VeryLow
}
+19
View File
@@ -0,0 +1,19 @@
namespace YesNt.Interpreter.Enums;
/// <summary>
/// Determines where in a source line the interpreter searches for a statement keyword.
/// </summary>
public enum SearchMode
{
/// <summary>The keyword must appear at the beginning of the line.</summary>
StartOfLine,
/// <summary>The keyword must appear at the end of the line.</summary>
EndOfLine,
/// <summary>The keyword may appear anywhere in the line.</summary>
Contains,
/// <summary>The entire line must exactly match the keyword.</summary>
Exact
}
@@ -0,0 +1,19 @@
namespace YesNt.Interpreter.Enums;
/// <summary>
/// Specifies which sides of a statement keyword must be surrounded by a space when matching.
/// </summary>
public enum SpaceAround
{
/// <summary>A space is required both before and after the keyword.</summary>
StartEnd,
/// <summary>A space is required before the keyword only.</summary>
Start,
/// <summary>A space is required after the keyword only.</summary>
End,
/// <summary>No surrounding spaces are required.</summary>
None
}
@@ -0,0 +1,13 @@
{
"profiles": {
"YesNt-Interpreter": {
"commandName": "Project",
"commandLineArgs": "code.ynt"
},
"WSL": {
"commandName": "WSL2",
"environmentVariables": {},
"distributionName": ""
}
}
}
@@ -0,0 +1,33 @@
using System;
namespace YesNt.Interpreter.Runtime;
/// <summary>
/// Provides per-line execution data raised through <see cref="YesNtInterpreter.OnLineExecuted"/>.
/// </summary>
public class DebugEventArgs : EventArgs
{
/// <summary>Gets the 1-based line number of the executed line within its source file.</summary>
public int LineNumber { get; internal set; }
/// <summary>
/// Gets the line content after all statement transformations have been applied
/// (e.g. after variable substitution). May differ from <see cref="OriginalLine"/>.
/// </summary>
public string CurrentLine { get; internal set; }
/// <summary>Gets the raw line content as it appeared in the source file.</summary>
public string OriginalLine { get; internal set; }
/// <summary>
/// Gets the task identifier of the task that executed this line, or <c>0</c> if the line
/// was executed on the main thread.
/// </summary>
public int TaskId { get; internal set; }
/// <summary>
/// Gets a value indicating whether this line was executed inside a background task
/// (spawned with the <c>task</c> statement).
/// </summary>
public bool IsTask { get; internal set; }
}
@@ -0,0 +1,83 @@
namespace YesNt.Interpreter.Runtime;
/// <summary>
/// Central repository of all exit/error message strings used by <see cref="RuntimeInformation.Exit"/>.
/// Keeping messages here ensures consistency and makes them easy to find or localise.
/// </summary>
internal static class ExitMessages
{
internal const string InvalidSyntax = "Invalid syntax";
internal const string InvalidSyntaxColonRequired = "Invalid syntax. Statement must end with ':'";
internal const string InvalidOperation = "Invalid operation";
internal const string InvalidStatement = "Invalid statement";
internal const string InvalidStringLiteral = "Invalid string literal";
internal const string EndOfFile = "End of file";
internal const string TerminatedByExternalProcess = "Terminated by external process";
internal const string TerminatedByChildTask = "Terminated by child task";
internal const string TerminatedByParentTask = "Terminated by parent task";
internal const string PlannedTermination = "Planned termination by code";
internal const string PlannedTerminationCancelingTasks = "Planned termination by code. Canceling all tasks";
internal const string NoMatchingEndIf = "No matching end_if found";
internal const string NoMatchingEndWhile = "No matching end_while found";
internal const string NoMatchingWhile = "No matching while found";
internal const string NestedFunctionsNotAllowed = "Nested functions are not allowed";
internal const string NoOutArgumentInStack = "No out argument in stack";
internal const string StatementNotAllowedOutsideFunction = "Statement not allowed outside of function";
internal const string NoInArgumentInStack = "No in argument in stack";
internal const string NoFunctionInStack = "No function in stack";
internal static string LabelNotFound(string label)
{
return $"Label \"{label}\" not found";
}
internal static string FunctionNotFound(string function)
{
return $"Function \"{function}\" not found";
}
internal static string VariableNotFound(string variable)
{
return $"Variable \"{variable}\" not found";
}
internal static string ListNotFound(string list)
{
return $"List \"{list}\" not found";
}
internal static string InvalidIndex(string rawIndex)
{
return $"\"{rawIndex}\" is not a valid index";
}
internal static string IndexOutOfRange(int index)
{
return $"Index {index} out of range";
}
internal static string InvalidTimeoutValue(string value)
{
return $"\"{value}\" is not a valid time-out value";
}
internal static string CouldNotLoadFile(string path)
{
return $"Could not load file \"{path}\"";
}
internal static string CouldNotFindFile(string path)
{
return $"Could not find file \"{path}\"";
}
internal static string CannotFindFile(string path)
{
return $"Cannot find file \"{path}\".";
}
internal static string FailedToStart(string program, string message)
{
return $"Failed to start \"{program}\". {message}";
}
}
@@ -0,0 +1,28 @@
using System.Collections.Generic;
namespace YesNt.Interpreter.Runtime;
/// <summary>
/// Represents one frame on the function call stack. Created when a <c>call</c> statement is
/// executed and popped when the matching <c>return</c> is reached.
/// </summary>
internal class FunctionScope(int callerLine, Stack<string> arguments)
{
/// <summary>Gets the zero-based line index to return to after this function completes.</summary>
public int CallerLine { get; } = callerLine;
/// <summary>Gets the local variable table for this function invocation.</summary>
public Dictionary<string, string> Variables { get; } = [];
/// <summary>Gets the local list table for this function invocation.</summary>
public Dictionary<string, List<string>> Lists { get; } = [];
/// <summary>Gets the local label table for this function invocation.</summary>
public Dictionary<string, int> Labels { get; } = [];
/// <summary>Gets the stack of input arguments passed to this function via <c>push_in</c>.</summary>
public Stack<string> Arguments { get; } = arguments;
/// <summary>Gets the stack of output values pushed via <c>push_out</c>, consumed by the caller via <c>%out</c>.</summary>
public Stack<string> Results { get; } = new();
}
@@ -0,0 +1,32 @@
using System.Collections.Generic;
namespace YesNt.Interpreter.Runtime;
/// <summary>
/// Exposes the script runtime state accessible to custom statement handlers registered
/// via <see cref="YesNtInterpreter.AddStatement"/>.
/// </summary>
public interface IStatementContext
{
/// <summary>Gets the local variable table for the current scope.</summary>
Dictionary<string, string> Variables { get; }
/// <summary>Gets or sets the global variable table shared across all scopes.</summary>
Dictionary<string, string> GlobalVariables { get; set; }
/// <summary>Gets or sets the text of the line currently being processed.
/// Inline-substitution handlers (e.g. <c>%read_line</c>) write their result here.</summary>
string CurrentLine { get; set; }
/// <summary>Gets or sets the zero-based index of the next line to execute.
/// Set this to implement control-flow jumps inside a custom statement.</summary>
int LineNumber { get; set; }
/// <summary>Terminates execution with the given message.</summary>
/// <param name="message">The message written to debug output.</param>
/// <param name="isError">
/// <see langword="true"/> to signal an error termination;
/// <see langword="false"/> for a planned, non-error termination.
/// </param>
void Exit(string message, bool isError);
}
+16
View File
@@ -0,0 +1,16 @@
namespace YesNt.Interpreter.Runtime;
/// <summary>
/// Represents a single source line together with its location metadata.
/// </summary>
internal class Line(string content, string fileName, int lineNumber)
{
/// <summary>Gets or sets the raw text content of the line.</summary>
public string Content { get; set; } = content;
/// <summary>Gets or sets the name of the source file this line originated from.</summary>
public string FileName { get; set; } = fileName;
/// <summary>Gets or sets the zero-based line index within <see cref="FileName"/>.</summary>
public int LineNumber { get; set; } = lineNumber;
}
@@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using System.Threading;
using YesNt.Interpreter.Utilities;
namespace YesNt.Interpreter.Runtime;
/// <summary>
/// Holds all mutable runtime state for a single script execution, including variables, lists,
/// labels, functions, the call stack, the line counter, and stop flags.
/// Each background task spawned by the <c>task</c> statement owns its own
/// <see cref="RuntimeInformation"/> whose <see cref="ParentRuntimeInformation"/> points back
/// to the main execution context.
/// </summary>
internal sealed class RuntimeInformation : IStatementContext
{
public event Action<string> OnDebugOutput;
public event Action<DebugEventArgs> OnLineExecuted;
private event Action<string, bool> OnExit;
private static int internalTaskId = 0;
private readonly Dictionary<string, string> topVariables = [];
private readonly Dictionary<string, List<string>> topLists = [];
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();
public List<Line> Lines { get; set; } = [];
public string CurrentLine { get; set; } = string.Empty;
public string SearchLabel { get; set; } = string.Empty;
public string SearchFunction { get; set; } = string.Empty;
public int LineNumber { get; set; } = 0;
public bool Stop { get; private set; } = false;
public bool StopAllTasks { get; private set; } = false;
public bool IsDebugMode { get; set; } = false;
public string WorkingDirectory { get; set; } = string.Empty;
public bool IsTask => ParentRuntimeInformation is not null;
public int TaskId { get => IsTask ? field : 0; private set; } = 0;
public bool InternalIsInFunction { get; set; }
public bool IsInFunction
{
get => InternalIsInFunction || FunctionCallStack.Count > 0;
set => InternalIsInFunction = value;
}
public Dictionary<string, string> Variables => FunctionCallStack.Count == 0 ? topVariables : FunctionCallStack.Peek().Variables;
public Dictionary<string, List<string>> Lists => FunctionCallStack.Count == 0 ? topLists : FunctionCallStack.Peek().Lists;
public Dictionary<string, int> Labels { get => FunctionCallStack.Count == 0 ? field : FunctionCallStack.Peek().Labels; } = [];
public RuntimeInformation ParentRuntimeInformation
{
get;
set
{
field = value;
field?.OnExit += ParentRuntimeInformation_OnExit;
}
}
public bool IsSearching => !string.IsNullOrWhiteSpace(SearchLabel + SearchFunction) || (IsInFunction && FunctionCallStack.Count == 0);
public bool IsLocalSearch { get; set; }
public void WriteLine(string output, bool forceWrite = false)
{
if ((Stop && !forceWrite) || (ParentRuntimeInformation?.StopAllTasks == true && !forceWrite))
{
return;
}
if (IsDebugMode)
{
if (IsTask)
{
ParentRuntimeInformation!.WriteLine(output.FromSafeString(), forceWrite);
}
else
{
OnDebugOutput?.Invoke(output.FromSafeString() + Environment.NewLine);
}
}
else
{
Console.WriteLine(output.FromSafeString());
}
}
public void Write(string output, bool forceWrite = false)
{
if ((Stop && !forceWrite) || (ParentRuntimeInformation?.StopAllTasks == true && !forceWrite))
{
return;
}
if (IsDebugMode)
{
if (IsTask)
{
ParentRuntimeInformation!.Write(output.FromSafeString(), forceWrite);
}
else
{
OnDebugOutput?.Invoke(output.FromSafeString());
}
}
else
{
Console.Write(output.FromSafeString());
}
}
public void Exit(string message, bool stopAllTasks)
{
if (!Stop)
{
if (Lines.Count == 0)
{
WriteLine($"{Environment.NewLine}[{(IsTask ? $"Task {TaskId}" : "The process")} was terminated with the message: {message}]", true);
}
else
{
Line line = Lines[Math.Min(LineNumber, Lines.Count - 1)];
WriteLine($"{Environment.NewLine}[{(IsTask ? $"Task {TaskId}" : "The process")} was terminated at line {line.LineNumber + 1} in the file \"{line.FileName}\" with the message: {message}]", true);
}
while (FunctionCallStack.Count > 0)
{
int stackLineNumber = FunctionCallStack.Pop().CallerLine;
List<Line> targetLines = ParentRuntimeInformation?.Lines ?? Lines;
if (stackLineNumber >= 0 && stackLineNumber < targetLines.Count)
{
Line stackLine = targetLines[stackLineNumber];
WriteLine($" at line {stackLine.LineNumber + 1} in the file \"{stackLine.FileName}\"", true);
}
else
{
WriteLine(" at unknown location (source not available)", true);
}
}
Stop = true;
}
if (stopAllTasks && !StopAllTasks)
{
StopAllTasks = true;
OnExit?.Invoke(message, StopAllTasks);
ParentRuntimeInformation?.Exit(ExitMessages.TerminatedByChildTask, true);
}
}
public void LineExecuted(DebugEventArgs debugEventArgs)
{
if (IsTask)
{
ParentRuntimeInformation.LineExecuted(debugEventArgs);
}
else
{
OnLineExecuted?.Invoke(debugEventArgs);
}
}
public void Reset()
{
topVariables.Clear();
topLists.Clear();
Lines.Clear();
GlobalVariables.Clear();
Labels.Clear();
Functions.Clear();
BlockBoundaries.Clear();
FunctionCallStack.Clear();
InParametersStack.Clear();
OutParametersStack.Clear();
ParentRuntimeInformation = null;
SearchLabel = string.Empty;
SearchFunction = string.Empty;
WorkingDirectory = string.Empty;
CurrentLine = string.Empty;
Stop = false;
StopAllTasks = false;
IsDebugMode = false;
IsInFunction = false;
IsLocalSearch = false;
LineNumber = 0;
#pragma warning disable S2696 // internalTaskId is a shared counter intentionally incremented by each Reset call
TaskId = Interlocked.Increment(ref internalTaskId);
#pragma warning restore S2696
}
private void ParentRuntimeInformation_OnExit(string exitMessage, bool stopAllTasks)
{
Exit(ExitMessages.TerminatedByParentTask, stopAllTasks);
}
}
@@ -0,0 +1,10 @@
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);
@@ -0,0 +1,35 @@
using System;
using YesNt.Interpreter.Enums;
namespace YesNt.Interpreter.Runtime;
/// <summary>
/// A read-only snapshot of a registered statement's metadata, used for tooling such as
/// syntax highlighters. Instances are obtained from <see cref="YesNtInterpreter.StatementInformation"/>.
/// </summary>
public class StatementInformation
{
/// <summary>Gets the keyword that identifies this statement in source code.</summary>
public string Name { get; internal set; }
/// <summary>Gets where in the line the keyword is searched for.</summary>
public SearchMode SearchMode { get; internal set; }
/// <summary>Gets which sides of the keyword must be padded with a space.</summary>
public SpaceAround SpaceAround { get; internal set; }
/// <summary>Gets the syntax-highlight color for this statement.</summary>
public ConsoleColor Color { get; internal set; }
/// <summary>
/// Gets a value indicating whether this statement is excluded from syntax highlighting.
/// </summary>
public bool IgnoreSyntaxHighlighting { get; internal set; }
/// <summary>
/// Gets the optional sub-string that must be present in the line for this statement to match,
/// or <see langword="null"/> if no separator is required.
/// </summary>
public string Separator { get; set; }
}
@@ -0,0 +1,24 @@
namespace YesNt.Interpreter.Runtime;
/// <summary>
/// Base class for all classes that host statement handler methods.
/// Subclasses declare methods decorated with <see cref="Attributes.StatementAttribute"/> or
/// <see cref="Attributes.StaticStatementAttribute"/>; the source generator
/// (<c>GeneratedStatementRegistry</c>) discovers these at compile time and wires them up.
/// </summary>
internal abstract class StatementRuntimeInformation
{
/// <summary>
/// Gets or sets the runtime state for the current execution context.
/// 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();
}
}
@@ -0,0 +1,549 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using YesNt.Interpreter.Attributes;
using YesNt.Interpreter.Enums;
using YesNt.Interpreter.Utilities;
namespace YesNt.Interpreter.Runtime;
/// <summary>
/// The main entry point for executing YesNt scripts.
/// </summary>
public class YesNtInterpreter
{
/// <summary>
/// Raised after each line is executed in debug mode. The argument is <see langword="null"/>
/// when execution ends (either normally or due to an error), allowing callers to detect completion.
/// </summary>
public event Action<DebugEventArgs> OnLineExecuted;
/// <summary>
/// Raised in debug mode whenever the script produces output (e.g. via <c>print_line</c>).
/// In non-debug mode output is written directly to <see cref="Console"/>.
/// </summary>
public event Action<string> OnDebugOutput;
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 = [];
/// <summary>
/// Gets a read-only snapshot of all currently registered statements.
/// Useful for building syntax highlighters or documentation tools.
/// </summary>
public ReadOnlyCollection<StatementInformation> StatementInformation
{
get
{
List<StatementInformation> information = statements.Select(s =>
{
return new StatementInformation()
{
Name = s.Key.Name,
SearchMode = s.Key.SearchMode,
SpaceAround = s.Key.SpaceAround,
Color = s.Key.Color,
IgnoreSyntaxHighlighting = s.Key.IgnoreSyntaxHighlighting,
Separator = s.Key.Separator
};
}).ToList();
return new ReadOnlyCollection<StatementInformation>(information);
}
}
/// <summary>
/// Initializes a new <see cref="YesNtInterpreter"/> and registers all built-in statements.
/// </summary>
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;
/// otherwise a new entry is added. Built-in statements use distinct attribute instances, so passing a
/// newly constructed attribute with the same name will <b>add</b> a second handler rather than replacing
/// the built-in. Use <see cref="RemoveStatement"/> first to replace a built-in keyword.
/// The statement list is re-sorted by priority after insertion.
/// </summary>
/// <param name="attribute">The attribute describing the keyword, search mode, and priority.</param>
/// <param name="handler">
/// The delegate invoked when the statement matches. Receives the argument text
/// (the part of the line after the keyword, unless <see cref="StatementAttribute.KeepStatementInArgs"/> is set).
/// </param>
public void AddStatement(StatementAttribute attribute, Action<string> handler)
{
statements[attribute] = handler;
statements = statements
.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 using a pre-built <see cref="StatementAttribute"/>,
/// with access to the script's <see cref="IStatementContext"/> (variables, line number, output, etc.).
/// </summary>
/// <param name="attribute">The attribute describing the keyword, search mode, and priority.</param>
/// <param name="handler">
/// The delegate invoked when the statement matches. Receives the argument text and the current
/// <see cref="IStatementContext"/> for reading/writing script state.
/// </param>
public void AddStatement(StatementAttribute attribute, Action<string, IStatementContext> handler)
{
AddStatement(attribute, args => handler(args, runtimeInfo));
}
/// <summary>
/// Registers a simple custom statement with default settings.
/// </summary>
/// <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)
{
AddStatement(new StatementAttribute(name, searchMode, spaceAround), handler);
}
/// <summary>
/// Registers a simple custom statement with default settings,
/// with access to the script's <see cref="IStatementContext"/> (variables, line number, output, etc.).
/// </summary>
/// <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. Receives the argument text and the current
/// <see cref="IStatementContext"/> for reading/writing script state.
/// </param>
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action<string, IStatementContext> handler)
{
AddStatement(new StatementAttribute(name, searchMode, spaceAround), handler);
}
/// <summary>
/// Registers a simple custom statement with a specific syntax-highlight color.
/// </summary>
/// <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)
{
AddStatement(new StatementAttribute(name, searchMode, spaceAround, consoleColor), handler);
}
/// <summary>
/// Registers a simple custom statement with a specific syntax-highlight color,
/// with access to the script's <see cref="IStatementContext"/> (variables, line number, output, etc.).
/// </summary>
/// <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. Receives the argument text and the current
/// <see cref="IStatementContext"/> for reading/writing script state.
/// </param>
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor consoleColor, Action<string, IStatementContext> handler)
{
AddStatement(new StatementAttribute(name, searchMode, spaceAround, consoleColor), handler);
}
/// <summary>
/// Unregisters all handlers matching the specified keyword <paramref name="name"/>.
/// </summary>
/// <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())
{
_ = statements.Remove(key);
}
_ = disabledStatements.Remove(name);
UpdateStatementHandlers();
PreScanLines();
}
/// <summary>
/// Disables all statements matching <paramref name="name"/> by replacing their handlers with
/// a no-op. The keyword still matches (so no "Invalid statement" error is raised), but the
/// statement has no effect. Use <see cref="EnableStatement"/> to restore original behavior.
/// </summary>
/// <param name="name">The keyword of the statement(s) to disable.</param>
public void DisableStatement(string name)
{
if (disabledStatements.ContainsKey(name))
{
return;
}
List<KeyValuePair<StatementAttribute, Action<string>>> matching =
statements.Where(kv => kv.Key.Name == name).ToList();
if (matching.Count == 0)
{
return;
}
disabledStatements[name] = matching;
foreach (KeyValuePair<StatementAttribute, Action<string>> kv in matching)
{
statements[kv.Key] = _ => { };
}
UpdateStatementHandlers();
PreScanLines();
}
/// <summary>
/// Re-enables statements previously disabled with <see cref="DisableStatement"/>,
/// restoring their original handlers.
/// Has no effect if the statement is not currently disabled.
/// </summary>
/// <param name="name">The keyword of the statement(s) to re-enable.</param>
public void EnableStatement(string name)
{
if (!disabledStatements.TryGetValue(name, out List<KeyValuePair<StatementAttribute, Action<string>>> saved))
{
return;
}
foreach (KeyValuePair<StatementAttribute, Action<string>> kv in saved)
{
statements[kv.Key] = kv.Value;
}
_ = disabledStatements.Remove(name);
UpdateStatementHandlers();
PreScanLines();
}
/// <summary>
/// Requests a graceful stop of the currently executing script.
/// The interpreter will terminate at the next line boundary.
/// </summary>
public void Stop()
{
runtimeInfo.Exit(ExitMessages.TerminatedByExternalProcess, true);
}
/// <summary>
/// Executes a YesNt script file.
/// </summary>
/// <param name="path">The path to the <c>.ynt</c> script file.</param>
/// <param name="isDebugMode">
/// When <see langword="true"/>, output is routed through <see cref="OnDebugOutput"/> instead of
/// <see cref="Console"/> and line-execution events are raised via <see cref="OnLineExecuted"/>.
/// </param>
public void Execute(string path, bool isDebugMode = false)
{
runtimeInfo.Reset();
runtimeInfo.IsDebugMode = isDebugMode;
if (LoadFile(path))
{
Execute();
}
}
/// <summary>
/// Executes a YesNt script supplied as an in-memory list of lines.
/// </summary>
/// <param name="lines">The script lines to execute.</param>
/// <param name="isDebugMode">
/// When <see langword="true"/>, output is routed through <see cref="OnDebugOutput"/> and
/// line-execution events are raised via <see cref="OnLineExecuted"/>.
/// </param>
public void Execute(List<string> lines, bool isDebugMode = false)
{
runtimeInfo.Reset();
runtimeInfo.IsDebugMode = isDebugMode;
for (int i = 0; i < lines.Count; i++)
{
string content = lines[i].Trim().Replace("\r", string.Empty);
runtimeInfo.Lines.Add(new Line(content, Path.GetFileName("#Memory#"), i));
}
PreScanLines();
Execute();
}
internal void Execute(List<Line> lines, Dictionary<string, string> globalVariables, int startLine, RuntimeInformation parentRuntimeInformation)
{
runtimeInfo.Reset();
runtimeInfo.IsDebugMode = parentRuntimeInformation.IsDebugMode;
runtimeInfo.Lines = lines;
runtimeInfo.LineNumber = startLine;
runtimeInfo.ParentRuntimeInformation = parentRuntimeInformation;
runtimeInfo.GlobalVariables = globalVariables;
if (parentRuntimeInformation.StopAllTasks)
{
runtimeInfo.Exit(ExitMessages.TerminatedByParentTask, parentRuntimeInformation.StopAllTasks);
return;
}
PreScanLines();
Execute();
}
private void Execute()
{
for (; runtimeInfo.LineNumber < runtimeInfo.Lines.Count; runtimeInfo.LineNumber++)
{
if (runtimeInfo.Stop)
{
break;
}
Line lineObj = runtimeInfo.Lines[runtimeInfo.LineNumber];
runtimeInfo.CurrentLine = lineObj.Content;
if (string.IsNullOrWhiteSpace(runtimeInfo.CurrentLine) || runtimeInfo.CurrentLine.StartsWith('#'))
{
continue;
}
DebugEventArgs debugEventArgs = null;
if (runtimeInfo.IsDebugMode)
{
debugEventArgs = new DebugEventArgs()
{
LineNumber = runtimeInfo.LineNumber + 1,
OriginalLine = runtimeInfo.CurrentLine.FromSafeString(),
IsTask = runtimeInfo.IsTask,
TaskId = runtimeInfo.TaskId
};
}
foreach (KeyValuePair<StaticStatementAttribute, Action> staticStatement in staticStatements)
{
StaticStatementAttribute staticStatementAttribute = staticStatement.Key;
if (!staticStatementAttribute.ExecuteInSearchMode && runtimeInfo.IsSearching)
{
continue;
}
staticStatement.Value.Invoke();
}
bool statementFound = false;
bool notSearchingLabel = !runtimeInfo.IsSearching;
List<StatementHandler> handlers = (runtimeInfo.LineNumber < lineMatchingHandlers.Count) ? lineMatchingHandlers[runtimeInfo.LineNumber] : [];
foreach (StatementHandler handler in handlers)
{
StatementAttribute statementAttribute = handler.Attribute;
if (!statementAttribute.ExecuteInSearchMode && runtimeInfo.IsSearching)
{
statementFound = true;
continue;
}
if (runtimeInfo.Stop)
{
break;
}
string name = handler.FullName;
if (statementAttribute.Separator is null || runtimeInfo.CurrentLine.Contains(statementAttribute.Separator, StringComparison.Ordinal))
{
if (statementAttribute.SearchMode == SearchMode.StartOfLine && runtimeInfo.CurrentLine.StartsWith(name, StringComparison.Ordinal))
{
string copyLine = statementAttribute.KeepStatementInArgs ? runtimeInfo.CurrentLine : runtimeInfo.CurrentLine[name.Length..];
handler.Handler.Invoke(copyLine);
statementFound = true;
}
else if (statementAttribute.SearchMode == SearchMode.Contains && runtimeInfo.CurrentLine.Contains(name, StringComparison.Ordinal))
{
string copyLine = statementAttribute.KeepStatementInArgs ? runtimeInfo.CurrentLine : runtimeInfo.CurrentLine.Replace(name, string.Empty);
handler.Handler.Invoke(copyLine);
statementFound = true;
}
else if (statementAttribute.SearchMode == SearchMode.EndOfLine && runtimeInfo.CurrentLine.EndsWith(name, StringComparison.Ordinal))
{
string copyLine = statementAttribute.KeepStatementInArgs ? runtimeInfo.CurrentLine : runtimeInfo.CurrentLine[..^name.Length];
handler.Handler.Invoke(copyLine);
statementFound = true;
}
else if (statementAttribute.SearchMode == SearchMode.Exact && runtimeInfo.CurrentLine.Equals(name, StringComparison.Ordinal))
{
handler.Handler.Invoke(runtimeInfo.CurrentLine);
statementFound = true;
}
}
}
if (!statementFound)
{
runtimeInfo.Exit(ExitMessages.InvalidStatement, true);
}
if (runtimeInfo.IsDebugMode && notSearchingLabel && debugEventArgs != null)
{
debugEventArgs.CurrentLine = runtimeInfo.CurrentLine.FromSafeString();
runtimeInfo.LineExecuted(debugEventArgs);
}
}
if (!runtimeInfo.Stop)
{
if (!string.IsNullOrWhiteSpace(runtimeInfo.SearchLabel))
{
runtimeInfo.Exit(ExitMessages.LabelNotFound(runtimeInfo.SearchLabel), true);
}
else if (!string.IsNullOrWhiteSpace(runtimeInfo.SearchFunction))
{
runtimeInfo.Exit(ExitMessages.FunctionNotFound(runtimeInfo.SearchFunction), true);
}
else
{
runtimeInfo.Exit(ExitMessages.EndOfFile, false);
}
if (runtimeInfo.IsDebugMode)
{
runtimeInfo.LineExecuted(null);
}
}
}
private bool LoadFile(string path)
{
path = Path.GetFullPath(path);
if (!File.Exists(path))
{
return false;
}
string[] lines = File.ReadAllLines(path);
runtimeInfo.WorkingDirectory = Path.GetDirectoryName(path);
for (int i = 0; i < lines.Length; 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
Dictionary<string, Stack<int>> openBlocks = [];
for (int i = 0; i < runtimeInfo.Lines.Count; i++)
{
string content = runtimeInfo.Lines[i].Content;
List<StatementHandler> matchingHandlers = [];
#pragma warning disable S3267 // foreach + if is intentional here; LINQ .Where() would add overhead in this scan loop
foreach (StatementHandler handler in statementHandlers)
{
if (IsPossibleMatch(content, handler))
#pragma warning restore S3267
{
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 Stack<int> stack))
{
stack = new Stack<int>();
openBlocks[blockPair] = stack;
}
stack.Push(i);
}
// Track block ends
if (handler.Attribute.IsBlockEnd && openBlocks.TryGetValue(handler.Attribute.Name, out Stack<int> endStack) && endStack.Count > 0)
{
int startLine = endStack.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 Stack<int> 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
};
}
}
@@ -0,0 +1,283 @@
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;
internal class CodeFlowStatements : StatementRuntimeInformation
{
[Statement("goto", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow)]
public void Jump(string args)
{
string key = NormalizeBlockName(args);
if (RuntimeInfo.Labels.TryGetValue(key, out int value))
{
RuntimeInfo.LineNumber = value;
}
else
{
RuntimeInfo.SearchLabel = key;
RuntimeInfo.IsLocalSearch = RuntimeInfo.IsInFunction;
}
}
[Statement("if", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow, Separator = " goto ")]
public void JumpIf(string args)
{
string[] parts = args.Split(" goto ", 2, StringSplitOptions.None);
if (parts.Length != 2)
{
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
return;
}
string condition = parts[0].Trim();
string key = NormalizeBlockName(parts[1]);
bool? result = Evaluator.EvaluateCondition(condition);
if (result is null)
{
RuntimeInfo.Exit(ExitMessages.InvalidOperation, true);
return;
}
if (result == false)
{
return;
}
if (RuntimeInfo.Labels.TryGetValue(key, out int value))
{
RuntimeInfo.LineNumber = value;
}
else
{
RuntimeInfo.SearchLabel = key;
RuntimeInfo.IsLocalSearch = RuntimeInfo.IsInFunction;
}
}
[Statement("label", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, ExecuteInSearchMode = true, Separator = ":")]
public void FindLabel(string args)
{
string labelDeclaration = args.Trim();
if (!labelDeclaration.EndsWith(':'))
{
RuntimeInfo.Exit(ExitMessages.InvalidSyntaxColonRequired, true);
return;
}
string key = NormalizeBlockName(labelDeclaration);
if (string.IsNullOrWhiteSpace(key))
{
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
return;
}
RuntimeInfo.Labels[key] = RuntimeInfo.LineNumber;
if (!string.IsNullOrWhiteSpace(RuntimeInfo.SearchLabel) && RuntimeInfo.SearchLabel == key)
{
RuntimeInfo.SearchLabel = string.Empty;
RuntimeInfo.IsLocalSearch = false;
}
}
[Statement("call", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, Priority = Priority.VeryLow)]
public void Call(string args)
{
CallFunction(NormalizeBlockName(args));
}
[Statement("if", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, Priority = Priority.VeryLow, Separator = " call ")]
public void CallIf(string args)
{
string[] parts = args.Split(" call ", 2, StringSplitOptions.None);
if (parts.Length != 2)
{
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
return;
}
string condition = parts[0].Trim();
bool? result = Evaluator.EvaluateCondition(condition);
if (result is null)
{
RuntimeInfo.Exit(ExitMessages.InvalidOperation, true);
return;
}
if (result == true)
{
CallFunction(NormalizeBlockName(parts[1]));
}
}
[Statement("if", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow, Separator = ":", BlockPair = "end_if")]
public void IfBlock(string args)
{
args = args.Trim();
if (!args.EndsWith(':'))
{
RuntimeInfo.Exit(ExitMessages.InvalidSyntaxColonRequired, true);
return;
}
string condition = args[..^1].Trim();
bool? result = Evaluator.EvaluateCondition(condition);
if (result is null)
{
RuntimeInfo.Exit(ExitMessages.InvalidOperation, true);
return;
}
if (result == true)
{
return;
}
int targetLine = FindBlockBoundary(RuntimeInfo.LineNumber);
if (targetLine < 0)
{
RuntimeInfo.Exit(ExitMessages.NoMatchingEndIf, true);
return;
}
RuntimeInfo.LineNumber = targetLine;
}
[Statement("else:", SearchMode.Exact, SpaceAround.None, ConsoleColor.Green, IsBlockIntermediate = true, BlockPair = "end_if")]
public void Else(string _)
{
int targetLine = FindBlockBoundary(RuntimeInfo.LineNumber);
if (targetLine < 0)
{
RuntimeInfo.Exit(ExitMessages.NoMatchingEndIf, true);
return;
}
RuntimeInfo.LineNumber = targetLine;
}
[Statement("end_if", SearchMode.Exact, SpaceAround.None, ConsoleColor.Green, IsBlockEnd = true)]
public void EndIf(string _)
{
// Intentionally empty: end_if is a block-boundary marker only; no runtime action needed.
}
[Statement("while", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow, Separator = ":", BlockPair = "end_while")]
public void While(string args)
{
args = args.Trim();
if (!args.EndsWith(':'))
{
RuntimeInfo.Exit(ExitMessages.InvalidSyntaxColonRequired, true);
return;
}
string condition = args[..^1].Trim();
bool? result = Evaluator.EvaluateCondition(condition);
if (result is null)
{
RuntimeInfo.Exit(ExitMessages.InvalidOperation, true);
return;
}
if (result == true)
{
return;
}
int endWhileLine = FindBlockBoundary(RuntimeInfo.LineNumber);
if (endWhileLine < 0)
{
RuntimeInfo.Exit(ExitMessages.NoMatchingEndWhile, true);
return;
}
RuntimeInfo.LineNumber = endWhileLine;
}
[Statement("end_while", SearchMode.Exact, SpaceAround.None, ConsoleColor.Green, IsBlockEnd = true)]
public void EndWhile(string _)
{
int whileLine = FindBlockBoundary(RuntimeInfo.LineNumber);
if (whileLine < 0)
{
RuntimeInfo.Exit(ExitMessages.NoMatchingWhile, true);
return;
}
RuntimeInfo.LineNumber = whileLine - 1;
}
[Statement("exit", SearchMode.Exact, SpaceAround.None, ConsoleColor.Red, ExecuteInSearchMode = true)]
public void End(string _)
{
HandleExit(ExitMessages.PlannedTermination, false);
}
[Statement("abort_all", SearchMode.Exact, SpaceAround.None, ConsoleColor.Red, ExecuteInSearchMode = true)]
public void Terminate(string _)
{
HandleExit(ExitMessages.PlannedTerminationCancelingTasks, true);
}
[Statement("throw", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Red)]
public void Throw(string message)
{
RuntimeInfo.Exit(message, true);
}
[Statement("error", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Red)]
public void Error(string message)
{
RuntimeInfo.Exit(message, false);
}
private void CallFunction(string key)
{
RuntimeInfo.FunctionCallStack.Push(new FunctionScope(RuntimeInfo.LineNumber, new Stack<string>(RuntimeInfo.InParametersStack)));
RuntimeInfo.InParametersStack.Clear();
if (RuntimeInfo.Functions.TryGetValue(key, out int value))
{
RuntimeInfo.LineNumber = value;
}
else
{
RuntimeInfo.SearchFunction = key;
}
}
private void HandleExit(string exitMessage, bool isError)
{
if (RuntimeInfo.IsSearching)
{
RuntimeInfo.IsInFunction = false;
if (RuntimeInfo.IsLocalSearch)
{
RuntimeInfo.Exit(ExitMessages.LabelNotFound(RuntimeInfo.SearchLabel), true);
}
return;
}
RuntimeInfo.IsInFunction = false;
RuntimeInfo.Exit(exitMessage, isError);
}
private int FindBlockBoundary(int currentLine)
{
return RuntimeInfo.BlockBoundaries.TryGetValue(currentLine, out int cached) ? cached : -1;
}
}
@@ -0,0 +1,66 @@
using System;
using System.Diagnostics.CodeAnalysis;
using YesNt.Interpreter.Attributes;
using YesNt.Interpreter.Enums;
using YesNt.Interpreter.Runtime;
using YesNt.Interpreter.Utilities;
namespace YesNt.Interpreter.Statements;
internal class ConsoleStatements : StatementRuntimeInformation
{
[Statement("print_line", SearchMode.Exact, SpaceAround.None, ConsoleColor.DarkGreen, Priority = Priority.VeryLow)]
public void WriteLineEmpty(string _)
{
RuntimeInfo.WriteLine(string.Empty);
}
[Statement("print_line", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkGreen, Priority = Priority.VeryLow)]
public void WriteLine(string args)
{
RuntimeInfo.WriteLine(args);
}
[Statement("print", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkGreen, Priority = Priority.VeryLow)]
public void Write(string args)
{
RuntimeInfo.Write(args);
}
[Statement("%read_line", SearchMode.Contains, SpaceAround.None, ConsoleColor.DarkGreen, KeepStatementInArgs = true, Priority = Priority.Highest)]
public void ReadLine(string args)
{
args += " ";
while (args.Contains("%read_line"))
{
string input = Console.ReadLine();
if (input is null)
{
RuntimeInfo.Exit(ExitMessages.TerminatedByExternalProcess, true);
return;
}
args = args.ReplaceFirstOccurrence("%read_line ", input.ToSafeString() + " ");
}
RuntimeInfo.CurrentLine = args.TrimEnd();
}
[Statement("%read_key", SearchMode.Contains, SpaceAround.None, ConsoleColor.DarkGreen, KeepStatementInArgs = true, Priority = Priority.Highest)]
public void ReadKey(string args)
{
args += " ";
while (args.Contains("%read_key"))
{
string input = ConsoleExtensions.ReadKey(RuntimeInfo).ToString();
args = args.ReplaceFirstOccurrence("%read_key ", input.ToSafeString() + " ");
}
RuntimeInfo.CurrentLine = args.TrimEnd();
}
[Statement("clear", SearchMode.Exact, SpaceAround.None, ConsoleColor.Magenta)]
[SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Won't work if static")]
public void Clear(string _)
{
Console.Clear();
}
}
@@ -0,0 +1,176 @@
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;
internal class FunctionStatements : StatementRuntimeInformation
{
[Statement("func", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, ExecuteInSearchMode = true, Separator = ":")]
public void FindFunction(string args)
{
if (RuntimeInfo.InternalIsInFunction)
{
RuntimeInfo.Exit(ExitMessages.NestedFunctionsNotAllowed, true);
return;
}
string functionDeclaration = args.Trim();
if (!functionDeclaration.EndsWith(':'))
{
RuntimeInfo.Exit(ExitMessages.InvalidSyntaxColonRequired, true);
return;
}
string key = NormalizeBlockName(functionDeclaration);
if (string.IsNullOrWhiteSpace(key))
{
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
return;
}
RuntimeInfo.Functions[key] = RuntimeInfo.LineNumber;
if (!string.IsNullOrWhiteSpace(RuntimeInfo.SearchFunction) && RuntimeInfo.SearchFunction == key)
{
RuntimeInfo.SearchFunction = string.Empty;
}
RuntimeInfo.IsInFunction = true;
}
[Statement("push_in", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Yellow)]
public void AddInParameter(string args)
{
RuntimeInfo.InParametersStack.Push(args);
}
[Statement("%out", SearchMode.Contains, SpaceAround.None, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)]
public void GetOutParameter(string args)
{
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)]
public void CheckIfOutParameterAvailable(string args)
{
args = args.Replace("%has_out", (RuntimeInfo.OutParametersStack.Count > 0).ToString());
RuntimeInfo.CurrentLine = args.TrimEnd();
}
[Statement("call", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, Priority = Priority.Low, Separator = " with ")]
public void Call(string args)
{
string[] parts = args.Split(" with ", 2, StringSplitOptions.None);
if (parts.Length != 2)
{
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
return;
}
string key = NormalizeBlockName(parts[0]);
string[] functionArguments = parts[1].Split(',');
foreach (string argument in functionArguments)
{
RuntimeInfo.InParametersStack.Push(argument.Trim());
}
RuntimeInfo.FunctionCallStack.Push(new FunctionScope(RuntimeInfo.LineNumber, new Stack<string>(RuntimeInfo.InParametersStack)));
RuntimeInfo.InParametersStack.Clear();
RuntimeInfo.CurrentLine = string.Empty;
if (RuntimeInfo.Functions.TryGetValue(key, out int value))
{
RuntimeInfo.LineNumber = value;
}
else
{
RuntimeInfo.SearchFunction = key;
}
}
[Statement("%in", SearchMode.Contains, SpaceAround.None, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)]
public void GetInParameter(string args)
{
if (!RuntimeInfo.IsInFunction)
{
RuntimeInfo.Exit(ExitMessages.StatementNotAllowedOutsideFunction, true);
return;
}
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)]
public void CheckIfInParameterAvailable(string args)
{
if (!RuntimeInfo.IsInFunction)
{
RuntimeInfo.Exit(ExitMessages.StatementNotAllowedOutsideFunction, true);
return;
}
args = args.Replace("%has_in", (RuntimeInfo.FunctionCallStack.Peek().Arguments.Count > 0).ToString());
RuntimeInfo.CurrentLine = args.TrimEnd();
}
[Statement("push_out", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Yellow)]
public void AddOutParameter(string args)
{
if (!RuntimeInfo.IsInFunction)
{
RuntimeInfo.Exit(ExitMessages.StatementNotAllowedOutsideFunction, true);
return;
}
RuntimeInfo.FunctionCallStack.Peek().Results.Push(args);
}
[Statement("return", SearchMode.Exact, SpaceAround.None, ConsoleColor.DarkYellow, ExecuteInSearchMode = true)]
public void Return(string _)
{
if (!RuntimeInfo.IsInFunction)
{
RuntimeInfo.Exit(ExitMessages.StatementNotAllowedOutsideFunction, true);
return;
}
if (RuntimeInfo.IsSearching)
{
RuntimeInfo.IsInFunction = false;
if (RuntimeInfo.IsLocalSearch)
{
RuntimeInfo.Exit(ExitMessages.LabelNotFound(RuntimeInfo.SearchLabel), true);
}
return;
}
RuntimeInfo.IsInFunction = false;
if (RuntimeInfo.FunctionCallStack.Count > 0)
{
FunctionScope functionScope = RuntimeInfo.FunctionCallStack.Pop();
RuntimeInfo.OutParametersStack = new Stack<string>(functionScope.Results);
RuntimeInfo.LineNumber = functionScope.CallerLine;
}
else
{
RuntimeInfo.Exit(ExitMessages.NoFunctionInStack, true);
}
}
[Statement("clear_call_stack", SearchMode.Exact, SpaceAround.None, ConsoleColor.Red)]
public void ClearCallStack(string _)
{
RuntimeInfo.FunctionCallStack.Clear();
}
}
@@ -0,0 +1,274 @@
using System;
using System.Collections.Generic;
using YesNt.Interpreter.Attributes;
using YesNt.Interpreter.Enums;
using YesNt.Interpreter.Runtime;
namespace YesNt.Interpreter.Statements;
internal class ListStatements : StatementRuntimeInformation
{
[Statement("list", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " new")]
public void Create(string args)
{
string[] parts = SplitTwo(args, " new");
if (parts.Length == 0)
{
return;
}
string name = parts[0];
if (!RuntimeInfo.Lists.TryGetValue(name, out List<string> value))
{
RuntimeInfo.Lists.Add(name, []);
}
else
{
value.Clear();
}
}
[Statement("list", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " delete")]
public void Delete(string args)
{
string[] parts = SplitTwo(args, " delete");
if (parts.Length == 0)
{
return;
}
string name = parts[0];
if (!RuntimeInfo.Lists.ContainsKey(name))
{
RuntimeInfo.Exit(ExitMessages.ListNotFound(name), true);
return;
}
_ = RuntimeInfo.Lists.Remove(name);
}
[Statement("list", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " clear")]
public void Clear(string args)
{
string[] parts = SplitTwo(args, " clear");
if (parts.Length == 0)
{
return;
}
if (!TryGetList(parts[0], out List<string> list))
{
return;
}
list.Clear();
}
[Statement("list", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " length")]
public void Length(string args)
{
string[] parts = SplitTwo(args, " length");
if (parts.Length == 0)
{
return;
}
if (!TryGetList(parts[0], out List<string> list))
{
return;
}
RuntimeInfo.OutParametersStack.Clear();
RuntimeInfo.OutParametersStack.Push(list.Count.ToString());
}
[Statement("list", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " add ")]
public void Add(string args)
{
string[] parts = SplitTwo(args, " add ");
if (parts.Length == 0)
{
return;
}
if (!TryGetList(parts[0], out List<string> list))
{
return;
}
if (string.IsNullOrWhiteSpace(parts[1]))
{
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
return;
}
list.Add(parts[1].Trim());
}
[Statement("list", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " get ")]
public void Get(string args)
{
string[] parts = SplitTwo(args, " get ");
if (parts.Length == 0)
{
return;
}
if (!TryGetList(parts[0], out List<string> list))
{
return;
}
if (!TryParseIndex(parts[1], out int index, list.Count))
{
return;
}
RuntimeInfo.OutParametersStack.Clear();
RuntimeInfo.OutParametersStack.Push(list[index]);
}
[Statement("list", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " remove ")]
public void Remove(string args)
{
string[] parts = SplitTwo(args, " remove ");
if (parts.Length == 0)
{
return;
}
if (!TryGetList(parts[0], out List<string> list))
{
return;
}
if (!TryParseIndex(parts[1], out int index, list.Count))
{
return;
}
list.RemoveAt(index);
}
[Statement("list", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " set ")]
public void Set(string args)
{
string[] parts = SplitTwo(args, " set ");
if (parts.Length == 0)
{
return;
}
if (!TryGetList(parts[0], out List<string> list))
{
return;
}
string[] indexAndValue = SplitIndexAndValue(parts[1]);
if (indexAndValue.Length == 0)
{
return;
}
if (!TryParseIndex(indexAndValue[0], out int index, list.Count))
{
return;
}
list[index] = indexAndValue[1];
}
[Statement("list", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " insert ")]
public void Insert(string args)
{
string[] parts = SplitTwo(args, " insert ");
if (parts.Length == 0)
{
return;
}
if (!TryGetList(parts[0], out List<string> list))
{
return;
}
string[] indexAndValue = SplitIndexAndValue(parts[1]);
if (indexAndValue.Length == 0)
{
return;
}
if (!TryParseIndex(indexAndValue[0], out int index, list.Count + 1))
{
return;
}
list.Insert(index, indexAndValue[1]);
}
private string[] SplitTwo(string input, string separator)
{
string[] parts = input.Split(separator, 2, StringSplitOptions.None);
if (parts.Length != 2)
{
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
return [];
}
parts[0] = parts[0].Trim();
parts[1] = parts[1].Trim();
if (string.IsNullOrWhiteSpace(parts[0]))
{
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
return [];
}
return parts;
}
private bool TryGetList(string name, out List<string> list)
{
if (!RuntimeInfo.Lists.TryGetValue(name, out list))
{
RuntimeInfo.Exit(ExitMessages.ListNotFound(name), true);
return false;
}
return true;
}
private string[] SplitIndexAndValue(string input)
{
string[] parts = input.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length != 2)
{
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
return [];
}
parts[0] = parts[0].Trim();
parts[1] = parts[1].Trim();
return parts;
}
private bool TryParseIndex(string rawIndex, out int index, int maxExclusive)
{
bool success = int.TryParse(rawIndex.Trim(), out index);
if (!success)
{
RuntimeInfo.Exit(ExitMessages.InvalidIndex(rawIndex), true);
return false;
}
if (index < 0 || index >= maxExclusive)
{
RuntimeInfo.Exit(ExitMessages.IndexOutOfRange(index), true);
return false;
}
return true;
}
}
@@ -0,0 +1,49 @@
using System;
using YesNt.Interpreter.Attributes;
using YesNt.Interpreter.Enums;
using YesNt.Interpreter.Runtime;
using YesNt.Interpreter.Utilities;
namespace YesNt.Interpreter.Statements;
internal class PredefinedVariableStatements : StatementRuntimeInformation
{
private readonly Random random = new Random();
[Statement("%time", SearchMode.Contains, SpaceAround.None, ConsoleColor.Blue, KeepStatementInArgs = true, Priority = Priority.Highest)]
public void GetUnixTimestamp(string args)
{
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)
{
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)
{
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)
{
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)
{
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)
{
RuntimeInfo.CurrentLine = TemplateProcessor.ProcessDynamicPlaceholders(args, "%rand", () => random.Next(32767, int.MaxValue).ToString()).TrimEnd();
}
}
@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using YesNt.Interpreter.Attributes;
using YesNt.Interpreter.Enums;
using YesNt.Interpreter.Runtime;
using YesNt.Interpreter.Utilities;
namespace YesNt.Interpreter.Statements;
internal partial class ProcessingStatements : StatementRuntimeInformation
{
[Statement("calc", SearchMode.EndOfLine, SpaceAround.Start, ConsoleColor.DarkYellow, Priority = Priority.High)]
public void Calculate(string args)
{
RuntimeInfo.CurrentLine = TemplateProcessor.ProcessCalculations(args.FromSafeString(), RuntimeInfo, CalculationRegex());
}
[Statement("eval", SearchMode.EndOfLine, SpaceAround.Start, ConsoleColor.DarkYellow, Priority = Priority.VeryHigh)]
public void Evaluate(string args)
{
RuntimeInfo.CurrentLine = args.FromSafeString();
}
[Statement("task", SearchMode.EndOfLine, SpaceAround.Start, ConsoleColor.DarkYellow, Priority = Priority.VeryHigh)]
public void RunTask(string line)
{
int lineNumber = RuntimeInfo.LineNumber;
List<Line> lines = RuntimeInfo.Lines.GetRange(0, RuntimeInfo.Lines.Count);
Line oldLine = lines[lineNumber];
lines[lineNumber] = new Line(line, oldLine.FileName, oldLine.LineNumber);
_ = Task.Run(() =>
{
YesNtInterpreter interpreter = new YesNtInterpreter();
interpreter.Execute(lines, RuntimeInfo.GlobalVariables, lineNumber, RuntimeInfo);
});
RuntimeInfo.CurrentLine = string.Empty;
}
[Statement("sleep", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Magenta)]
public void Sleep(string args)
{
if (int.TryParse(args, out int millisecondsTimeout))
{
ConsoleExtensions.Sleep(millisecondsTimeout, RuntimeInfo);
}
else
{
RuntimeInfo.Exit(ExitMessages.InvalidTimeoutValue(args), true);
}
}
[Statement("length", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Magenta)]
public void Length(string args)
{
RuntimeInfo.InParametersStack.Clear();
RuntimeInfo.OutParametersStack.Clear();
RuntimeInfo.OutParametersStack.Push(args.FromSafeString().Length.ToString());
}
[Statement("import", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Magenta)]
public void Import(string path)
{
path = path.FromSafeString();
path = Path.Combine(RuntimeInfo.WorkingDirectory, path);
if (string.IsNullOrEmpty(Path.GetExtension(path)))
{
path = Path.ChangeExtension(path, "ynt");
}
if (File.Exists(path))
{
try
{
RuntimeInfo.Lines.RemoveAt(RuntimeInfo.LineNumber);
string[] lines = File.ReadAllLines(path);
for (int i = 0; i < lines.Length; i++)
{
RuntimeInfo.Lines.Insert(RuntimeInfo.LineNumber + i, new Line(lines[i], Path.GetFileName(path), i));
}
RuntimeInfo.PreScanLinesAction?.Invoke();
RuntimeInfo.LineNumber--;
}
catch
{
RuntimeInfo.Exit(ExitMessages.CouldNotLoadFile(path), true);
}
}
else
{
RuntimeInfo.Exit(ExitMessages.CouldNotFindFile(path), true);
}
}
[GeneratedRegex("[0-9*+().,^%/-]+[0-9*+ ().,^%/-]+[0-9*+().,^%/-]+")]
private static partial Regex CalculationRegex();
}
@@ -0,0 +1,84 @@
using System.Text;
using YesNt.Interpreter.Attributes;
using YesNt.Interpreter.Enums;
using YesNt.Interpreter.Runtime;
using YesNt.Interpreter.Utilities;
namespace YesNt.Interpreter.Statements;
internal class StringLiteralStatements : StatementRuntimeInformation
{
[Statement("\"", SearchMode.Contains, SpaceAround.None, System.ConsoleColor.DarkYellow, Priority = Priority.PreProcessing, KeepStatementInArgs = true)]
public void ParseStringLiterals(string args)
{
if (!args.Contains('"'))
{
return;
}
StringBuilder output = new StringBuilder(args.Length);
#pragma warning disable S127 // i is intentionally advanced to track position within quoted literals and escape sequences
for (int i = 0; i < args.Length; i++)
{
char current = args[i];
if (current != '"')
{
_ = output.Append(current);
continue;
}
StringBuilder literal = new StringBuilder();
bool closed = false;
i++;
for (; i < args.Length; i++)
{
char ch = args[i];
if (ch == '\\' && i + 1 < args.Length)
{
i++;
_ = literal.Append(ParseEscape(args[i]));
continue;
}
if (ch == '"')
{
closed = true;
break;
}
_ = literal.Append(ch);
}
#pragma warning restore S127
if (!closed)
{
RuntimeInfo.Exit(ExitMessages.InvalidStringLiteral, true);
return;
}
_ = output.Append(literal.ToString().ToSafeString());
}
RuntimeInfo.CurrentLine = output.ToString();
}
private static char ParseEscape(char escapeChar)
{
return escapeChar switch
{
'n' => '\n',
'r' => '\r',
't' => '\t',
'b' => '\b',
'f' => '\f',
'a' => '\a',
'v' => '\v',
'"' => '"',
'\\' => '\\',
_ => escapeChar
};
}
}
@@ -0,0 +1,119 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using YesNt.Interpreter.Attributes;
using YesNt.Interpreter.Enums;
using YesNt.Interpreter.Runtime;
using YesNt.Interpreter.Utilities;
namespace YesNt.Interpreter.Statements;
internal class SystemStatements : StatementRuntimeInformation
{
[Statement("exec", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Magenta, Priority = Priority.Low, Separator = " with ")]
public void ExecuteProgramWithArgs(string input)
{
string[] parts = input.FromSafeString().Split(" with ", 2, StringSplitOptions.None);
string program = parts[0].Trim();
foreach (string argument in parts[1].Split(','))
{
RuntimeInfo.InParametersStack.Push(argument.Trim());
}
try
{
StartProcess(program, string.Join(" ", RuntimeInfo.InParametersStack.Reverse()));
}
catch (FileNotFoundException)
{
RuntimeInfo.Exit(ExitMessages.CannotFindFile(program), false);
}
catch (Win32Exception ex)
{
RuntimeInfo.Exit(ExitMessages.FailedToStart(program, ex.Message), false);
}
}
[Statement("exec", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Magenta, Priority = Priority.VeryLow)]
public void ExecuteProgram(string input)
{
// This is to prevent the "exec with" statement from being triggered by this one, since they both start with "exec"
if (input.Contains(" with "))
{
return;
}
input = input.FromSafeString();
try
{
StartProcess(input, string.Join(" ", RuntimeInfo.InParametersStack.Reverse()));
}
catch (FileNotFoundException)
{
RuntimeInfo.Exit(ExitMessages.CannotFindFile(input), false);
}
catch (Win32Exception ex)
{
RuntimeInfo.Exit(ExitMessages.FailedToStart(input, ex.Message), false);
}
}
private void Process_ErrorDataReceived(Utilities.DataReceivedEventArgs e, Stack<string> outputStack)
{
if (string.IsNullOrWhiteSpace(e.Data))
{
return;
}
outputStack.Push(e.Data.ToSafeString());
RuntimeInfo.Write("Error: " + e.Data);
}
private void Process_OutputDataReceived(Utilities.DataReceivedEventArgs e, Stack<string> outputStack)
{
if (string.IsNullOrWhiteSpace(e.Data))
{
return;
}
outputStack.Push(e.Data.ToSafeString());
RuntimeInfo.Write(e.Data);
}
private void StartProcess(string name, string args)
{
RuntimeInfo.OutParametersStack.Clear();
Stack<string> outputStack = new Stack<string>();
FixedProcess process = new FixedProcess
{
StartInfo = new ProcessStartInfo()
{
FileName = name,
Arguments = args,
RedirectStandardOutput = true,
RedirectStandardError = true
}
};
process.OutputDataReceived += (s, e) => Process_OutputDataReceived(e, outputStack);
process.ErrorDataReceived += (s, e) => Process_ErrorDataReceived(e, outputStack);
_ = process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
process.WaitForExit();
RuntimeInfo.InParametersStack.Clear();
RuntimeInfo.OutParametersStack = new(outputStack);
RuntimeInfo.OutParametersStack.Push(process.ExitCode.ToString());
}
}
@@ -0,0 +1,67 @@
using System.Collections.Generic;
using YesNt.Interpreter.Attributes;
using YesNt.Interpreter.Enums;
using YesNt.Interpreter.Runtime;
using YesNt.Interpreter.Utilities;
namespace YesNt.Interpreter.Statements;
internal partial class VariableStatements : StatementRuntimeInformation
{
[Statement("var", SearchMode.StartOfLine, SpaceAround.End, System.ConsoleColor.DarkBlue, Priority = Priority.VeryLow, Separator = "=")]
public void DefineVariable(string args)
{
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)
{
string key = parts[0].Trim();
if (key.Contains(' '))
{
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
}
dict[key] = parts[1].Trim();
}
else
{
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
}
}
[Statement("delete", SearchMode.StartOfLine, SpaceAround.End, System.ConsoleColor.Red, Priority = Priority.VeryLow)]
public void DeleteVariable(string args)
{
string key = args.Trim();
if (RuntimeInfo.Variables.ContainsKey(key))
{
_ = RuntimeInfo.Variables.Remove(key);
}
else if (RuntimeInfo.GlobalVariables.ContainsKey(key))
{
_ = RuntimeInfo.GlobalVariables.Remove(key);
}
else
{
RuntimeInfo.Exit(ExitMessages.VariableNotFound(key), true);
}
}
[Statement("${", SearchMode.Contains, SpaceAround.None, Priority = Priority.Highest, Separator = "}")]
public void ReadVariable(string _)
{
RuntimeInfo.CurrentLine = TemplateProcessor.ProcessVariables(RuntimeInfo.CurrentLine, RuntimeInfo);
}
}
@@ -0,0 +1,37 @@
using System;
using System.Diagnostics;
using System.Threading;
using YesNt.Interpreter.Runtime;
namespace YesNt.Interpreter.Utilities;
internal static class ConsoleExtensions
{
public static char ReadKey(RuntimeInformation runtimeInformation)
{
while (!runtimeInformation.Stop)
{
if (Console.KeyAvailable)
{
return Console.ReadKey().KeyChar;
}
Thread.Sleep(10);
}
return ' ';
}
public static void Sleep(int millisecondsTimeout, RuntimeInformation runtimeInformation)
{
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
while (!runtimeInformation.Stop)
{
if (stopwatch.ElapsedMilliseconds > millisecondsTimeout)
{
return;
}
Thread.Sleep(10);
}
}
}
@@ -0,0 +1,217 @@
using System;
using System.Linq;
using System.Text.RegularExpressions;
namespace YesNt.Interpreter.Utilities;
/// <summary>
/// Provides expression evaluation used by conditional and arithmetic statements.
/// </summary>
internal static partial class Evaluator
{
/// <summary>
/// Evaluates a boolean condition string such as <c>a == b</c>, <c>x &gt; 3</c>, or <c>true</c>.
/// </summary>
/// <param name="input">The condition expression, which may contain safe-string encoded values.</param>
/// <returns>
/// <see langword="true"/> or <see langword="false"/> if the condition could be evaluated;
/// <see langword="null"/> if the expression is not a recognized condition form (treated as an error by callers).
/// </returns>
public static bool? EvaluateCondition(string input)
{
input = input.FromSafeString();
string lower = input.ToLower().Trim();
if (lower == "true")
{
return true;
}
else if (lower == "false")
{
return false;
}
string[] parts = input.Split("==");
if (parts.Length == 2)
{
string part1 = parts[0].Trim();
string part2 = parts[1].Trim();
return part1 == part2;
}
parts = input.Split("!=");
if (parts.Length == 2)
{
string part1 = parts[0].Trim();
string part2 = parts[1].Trim();
return part1 != part2;
}
parts = input.Split(">=");
if (parts.Length == 2)
{
bool succ1 = parts[0].ToStandardizedNumber(out double part1);
bool succ2 = parts[1].ToStandardizedNumber(out double part2);
return succ1 && succ2 && part1 >= part2;
}
parts = input.Split("<=");
if (parts.Length == 2)
{
bool succ1 = parts[0].ToStandardizedNumber(out double part1);
bool succ2 = parts[1].ToStandardizedNumber(out double part2);
return succ1 && succ2 && part1 <= part2;
}
parts = input.Split(">");
if (parts.Length == 2)
{
bool succ1 = parts[0].ToStandardizedNumber(out double part1);
bool succ2 = parts[1].ToStandardizedNumber(out double part2);
return succ1 && succ2 && part1 > part2;
}
parts = input.Split("<");
if (parts.Length == 2)
{
bool succ1 = parts[0].ToStandardizedNumber(out double part1);
bool succ2 = parts[1].ToStandardizedNumber(out double part2);
return succ1 && succ2 && part1 < part2;
}
return null;
}
/// <summary>
/// Evaluates a numeric arithmetic expression string and returns the result as a string.
/// Supports <c>+</c>, <c>-</c>, <c>*</c>, <c>/</c>, <c>%</c> (modulo), and <c>^</c> (power) operators
/// with standard precedence (<c>^</c> highest, <c>+</c>/<c>-</c> lowest) and parentheses.
/// Adjacent sign characters (<c>++</c>, <c>--</c>, <c>-+</c>, <c>+-</c>) are normalized before evaluation.
/// </summary>
/// <param name="input">The arithmetic expression to evaluate.</param>
/// <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, "-");
return CalculateInternal(input, '+');
}
private static string CalculateInternal(string input, char op)
{
if (string.IsNullOrWhiteSpace(input))
{
return null;
}
if (input.ToStandardizedNumber(out double quickNum))
{
return quickNum.ToString(System.Globalization.CultureInfo.InvariantCulture);
}
MatchCollection matches = ParenthesesRegex().Matches(input);
while (matches.Count > 0)
{
for (int i = 0; i < matches.Count; i++)
{
string calc = matches[i].Value.Substring(1, matches[i].Length - 2);
string ret = CalculateInternal(calc, '+');
input = input.Replace(matches[i].Value, ret);
}
matches = ParenthesesRegex().Matches(input);
}
string[] parts = input.Split(op);
// If the expression starts with the operator (e.g. "-3 + 5" split by '-' gives ["", "3 + 5"]),
// prepend the operator back onto the first real part so it isn't lost.
if (parts.Length >= 2 && string.IsNullOrWhiteSpace(parts[0]))
{
parts[1] = $"{op}{parts[1]}";
parts = parts.Skip(1).ToArray();
}
double number = double.NaN;
foreach (string p in parts)
{
string part = p;
part = op switch
{
'+' => CalculateInternal(part, '-'),
'-' => CalculateInternal(part, '*'),
'*' => CalculateInternal(part, '/'),
'/' => CalculateInternal(part, '%'),
'%' => CalculateInternal(part, '^'),
_ => part
};
if (part is null)
{
return null;
}
if (part.ToStandardizedNumber(out double num))
{
if (double.IsNaN(number))
{
number = num;
}
else
{
switch (op)
{
case '+':
number += num;
break;
case '-':
number -= num;
break;
case '*':
number *= num;
break;
case '/':
number /= num;
break;
case '%':
number %= num;
break;
case '^':
number = Math.Pow(number, num);
break;
}
}
}
else
{
return null;
}
}
return number.ToString(System.Globalization.CultureInfo.InvariantCulture);
}
[GeneratedRegex("\\(([^()]+)\\)")]
private static partial Regex ParenthesesRegex();
[GeneratedRegex("(\\+ +\\+)+")]
private static partial Regex PlusPlusRegex();
[GeneratedRegex("(\\- +\\-)+")]
private static partial Regex MinusMinusRegex();
[GeneratedRegex("(\\- +\\+)+")]
private static partial Regex MinusPlusRegex();
[GeneratedRegex("(\\+ +\\-)+")]
private static partial Regex PlusMinusRegex();
}
@@ -0,0 +1,323 @@
using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
namespace YesNt.Interpreter.Utilities;
/// <summary>Represents the method that handles the <see cref="FixedProcess.OutputDataReceived"/> and <see cref="FixedProcess.ErrorDataReceived"/> events.</summary>
public delegate void DataReceivedEventHandler(object sender, DataReceivedEventArgs e);
internal delegate void UserCallBack(string data);
/// <summary>
/// A workaround replacement for <see cref="System.Diagnostics.Process"/> that fixes a buffering
/// issue in <see cref="System.Diagnostics.Process.BeginOutputReadLine"/> / <see cref="System.Diagnostics.Process.BeginErrorReadLine"/>:
/// the BCL implementation only delivers data when a newline is encountered, which means partial
/// lines are not raised until the process writes another newline or exits.
/// <see cref="FixedProcess"/> flushes whatever is in the read buffer immediately, enabling real-time
/// output forwarding for interactive child processes.
/// </summary>
internal class FixedProcess : Process
{
public new event DataReceivedEventHandler OutputDataReceived;
public new event DataReceivedEventHandler ErrorDataReceived;
internal AsyncStreamReader output;
internal AsyncStreamReader error;
public new void BeginOutputReadLine()
{
Stream baseStream = StandardOutput.BaseStream;
output = new AsyncStreamReader(baseStream, new UserCallBack(FixedOutputReadNotifyUser), StandardOutput.CurrentEncoding);
output.BeginReadLine();
}
public new void BeginErrorReadLine()
{
Stream baseStream = StandardError.BaseStream;
error = new AsyncStreamReader(baseStream, new UserCallBack(FixedErrorReadNotifyUser), StandardError.CurrentEncoding);
error.BeginReadLine();
}
internal void FixedOutputReadNotifyUser(string data)
{
DataReceivedEventHandler outputDataReceived = OutputDataReceived;
if (outputDataReceived != null)
{
DataReceivedEventArgs dataReceivedEventArgs = new DataReceivedEventArgs(data);
if (SynchronizingObject != null && SynchronizingObject.InvokeRequired)
{
_ = SynchronizingObject.Invoke(outputDataReceived,
[
this,
dataReceivedEventArgs
]);
return;
}
outputDataReceived(this, dataReceivedEventArgs);
}
}
internal void FixedErrorReadNotifyUser(string data)
{
DataReceivedEventHandler errorDataReceived = ErrorDataReceived;
if (errorDataReceived != null)
{
DataReceivedEventArgs dataReceivedEventArgs = new DataReceivedEventArgs(data);
if (SynchronizingObject != null && SynchronizingObject.InvokeRequired)
{
_ = SynchronizingObject.Invoke(errorDataReceived,
[
this,
dataReceivedEventArgs
]);
return;
}
errorDataReceived(this, dataReceivedEventArgs);
}
}
}
/// <summary>Provides data for the <see cref="FixedProcess.OutputDataReceived"/> and <see cref="FixedProcess.ErrorDataReceived"/> events.</summary>
public class DataReceivedEventArgs : EventArgs
{
internal string _data;
/// <summary>Gets the line of characters that was written to a redirected <see cref="T:System.Diagnostics.Process" /> output stream.</summary>
/// <returns>The line that was written by an associated <see cref="T:System.Diagnostics.Process" /> to its redirected <see cref="P:System.Diagnostics.Process.StandardOutput" /> or <see cref="P:System.Diagnostics.Process.StandardError" /> stream.</returns>
/// <filterpriority>2</filterpriority>
public string Data => _data;
internal DataReceivedEventArgs(string data)
{
_data = data;
}
}
internal class AsyncStreamReader : IDisposable
{
internal const int DefaultBufferSize = 1024;
private readonly Queue messageQueue;
private Stream stream;
private Encoding encoding;
private Decoder decoder;
private byte[] byteBuffer;
private char[] charBuffer;
private UserCallBack userCallBack;
private bool cancelOperation;
private ManualResetEvent eofEvent;
private StringBuilder sb;
private bool bLastCarriageReturn;
public virtual Encoding CurrentEncoding => encoding;
public virtual Stream BaseStream => stream;
internal AsyncStreamReader(Stream stream, UserCallBack callback, Encoding encoding) : this(stream, callback, encoding, 1024)
{
}
internal AsyncStreamReader(Stream stream, UserCallBack callback, Encoding encoding, int bufferSize)
{
Init(stream, callback, encoding, bufferSize);
messageQueue = new Queue();
}
public virtual void Close()
{
Dispose(true);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
internal void BeginReadLine()
{
if (cancelOperation)
{
cancelOperation = false;
}
if (sb == null)
{
sb = new StringBuilder(1024);
_ = stream.BeginRead(byteBuffer, 0, byteBuffer.Length, new AsyncCallback(ReadBuffer), null);
return;
}
FlushMessageQueue();
}
internal void CancelOperation()
{
cancelOperation = true;
}
internal void WaitUtilEOF()
{
if (eofEvent != null)
{
_ = eofEvent.WaitOne();
eofEvent.Close();
eofEvent = null;
}
}
protected virtual void Dispose(bool disposing)
{
if (disposing && stream != null)
{
stream.Close();
}
if (stream != null)
{
stream = null;
encoding = null;
decoder = null;
byteBuffer = null;
charBuffer = null;
}
eofEvent?.Close();
eofEvent = null;
}
private void Init(Stream stream, UserCallBack callback, Encoding encoding, int bufferSize)
{
this.stream = stream;
this.encoding = encoding;
userCallBack = callback;
decoder = encoding.GetDecoder();
if (bufferSize < 128)
{
bufferSize = 128;
}
byteBuffer = new byte[bufferSize];
int _maxCharsPerBuffer = encoding.GetMaxCharCount(bufferSize);
charBuffer = new char[_maxCharsPerBuffer];
cancelOperation = false;
eofEvent = new ManualResetEvent(false);
sb = null;
bLastCarriageReturn = false;
}
private void ReadBuffer(IAsyncResult ar)
{
int num;
try
{
num = stream.EndRead(ar);
}
catch (IOException)
{
num = 0;
}
catch (OperationCanceledException)
{
num = 0;
}
if (num == 0)
{
lock (messageQueue)
{
if (sb.Length != 0)
{
messageQueue.Enqueue(sb.ToString());
sb.Length = 0;
}
messageQueue.Enqueue(null);
}
try
{
FlushMessageQueue();
return;
}
finally
{
_ = eofEvent.Set();
}
}
int chars = decoder.GetChars(byteBuffer, 0, num, charBuffer, 0);
_ = sb.Append(charBuffer, 0, chars);
GetLinesFromStringBuilder();
_ = stream.BeginRead(byteBuffer, 0, byteBuffer.Length, new AsyncCallback(ReadBuffer), null);
}
private void GetLinesFromStringBuilder()
{
int i = 0;
int num = 0;
int length = sb.Length;
if (bLastCarriageReturn && length > 0 && sb[0] == '\n')
{
i = 1;
num = 1;
bLastCarriageReturn = false;
}
while (i < length)
{
char c = sb[i];
if (c is '\r' or '\n')
{
if (c == '\r' && i + 1 < length && sb[i + 1] == '\n')
{
i++;
}
string obj = sb.ToString(num, i + 1 - num);
num = i + 1;
lock (messageQueue)
{
messageQueue.Enqueue(obj);
}
}
i++;
}
// Flush Fix: Send Whatever is left in the buffer
string endOfBuffer = sb.ToString(num, length - num);
lock (messageQueue)
{
messageQueue.Enqueue(endOfBuffer);
num = length;
}
// End Flush Fix
if (sb[length - 1] == '\r')
{
bLastCarriageReturn = true;
}
if (num < length)
{
_ = sb.Remove(0, num);
}
else
{
sb.Length = 0;
}
FlushMessageQueue();
}
private void FlushMessageQueue()
{
while (messageQueue.Count > 0)
{
lock (messageQueue)
{
if (messageQueue.Count > 0)
{
string data = (string)messageQueue.Dequeue();
if (!cancelOperation)
{
userCallBack(data);
}
}
}
}
}
}
@@ -0,0 +1,180 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
namespace YesNt.Interpreter.Utilities;
/// <summary>
/// Extension methods for string manipulation used throughout the interpreter.
/// </summary>
/// <remarks>
/// <para>
/// YesNt uses a "safe string" encoding to pass values through the interpreter pipeline without
/// accidentally triggering keyword matching. Special characters (spaces, operators, punctuation,
/// control characters) are replaced with SOH-delimited three-letter codes
/// (e.g. space → <c>\x01spc\x01</c>, newline → <c>\x01nli\x01</c>). These codes use the
/// non-printable SOH character (U+0001) as a sentinel. Written via string concatenation
/// (<c>"\x01" + "spc" + "\x01"</c>) to avoid C#'s greedy <c>\x</c> hex escape absorbing
/// following hex-digit letters. U+0001 cannot appear in normal user source, preventing raw
/// source text from being accidentally decoded.
/// The mapping is defined in <see cref="ReplacementRules"/>. Use <see cref="ToSafeString"/>
/// to encode and <see cref="FromSafeString"/> to decode.
/// </para>
/// </remarks>
public static class StringExtensions
{
/// <summary>
/// Gets the table that maps special characters to their safe-string escape codes.
/// Keys are the original characters; values are the three-letter tilde codes.
/// </summary>
public static Dictionary<string, string> ReplacementRules { get; } = new()
{
{"~", "\x01" + "til" + "\x01" },
{" ", "\x01" + "spc" + "\x01" },
{"%", "\x01" + "per" + "\x01" },
{"<", "\x01" + "let" + "\x01" },
{">", "\x01" + "grt" + "\x01" },
{",", "\x01" + "com" + "\x01" },
{"!", "\x01" + "exm" + "\x01" },
{"|", "\x01" + "pip" + "\x01" },
{"\n", "\x01" + "nli" + "\x01" },
{"\r", "\x01" + "ret" + "\x01" },
{"\t", "\x01" + "tab" + "\x01" },
{"\b", "\x01" + "bac" + "\x01" },
{"\f", "\x01" + "for" + "\x01" },
{"\a", "\x01" + "ale" + "\x01" },
{"", "\x01" + "emp" + "\x01" },
};
private static readonly Dictionary<string, string> reverseReplacementRules = ReplacementRules.ToDictionary(x => x.Value, x => x.Key);
/// <summary>
/// Encodes a string into safe-string format so that special characters cannot accidentally
/// trigger interpreter keyword matching. Each character is wrapped with vertical-tab sentinels
/// before rule substitution so that multi-character replacements do not overlap.
/// </summary>
/// <param name="input">The plain string to encode.</param>
/// <returns>The safe-string encoded representation.</returns>
public static string ToSafeString(this string input)
{
if (string.IsNullOrEmpty(input))
{
return input;
}
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>
/// Decodes a safe-string back to its original plain-text form.
/// </summary>
/// <param name="input">A safe-string encoded string.</param>
/// <returns>The decoded plain string.</returns>
public static string FromSafeString(this string input)
{
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);
#pragma warning disable S127 // i is intentionally advanced by 4 when a 5-char escape code is consumed
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]);
}
#pragma warning restore S127
return output.ToString();
}
/// <summary>
/// Tries to parse the string as a <see cref="double"/>, first decoding safe-string encoding
/// and normalising decimal separators (comma → period).
/// </summary>
/// <param name="input">The string to parse (may be safe-string encoded).</param>
/// <param name="result">When this method returns, contains the parsed value if successful.</param>
/// <returns><see langword="true"/> if parsing succeeded; otherwise <see langword="false"/>.</returns>
public static bool ToStandardizedNumber(this string input, out double 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>
/// <param name="input">The source string.</param>
/// <param name="oldValue">The substring to find.</param>
/// <param name="newValue">The replacement value.</param>
/// <returns>A new string with the first occurrence replaced.</returns>
public static string ReplaceFirstOccurrence(this string input, string oldValue, string newValue)
{
int place = input.IndexOf(oldValue);
return input.Remove(place, oldValue.Length).Insert(place, newValue);
}
/// <summary>Replaces only the last occurrence of <paramref name="oldValue"/> in the string.</summary>
/// <param name="input">The source string.</param>
/// <param name="oldValue">The substring to find.</param>
/// <param name="newValue">The replacement value.</param>
/// <returns>A new string with the last occurrence replaced.</returns>
public static string ReplaceLastOccurrence(this string input, string oldValue, string newValue)
{
int place = input.LastIndexOf(oldValue);
return input.Remove(place, Math.Min(oldValue.Length, input.Length - place)).Insert(place, newValue);
}
/// <summary>Counts the number of trailing whitespace characters in the string.</summary>
/// <param name="input">The source string.</param>
/// <returns>The number of whitespace characters at the end of the string.</returns>
public static int WhiteSpaceAtEnd(this string input)
{
int count = 0;
int index = input.Length - 1;
while (index >= 0 && char.IsWhiteSpace(input[index--]))
{
count++;
}
return count;
}
}
@@ -0,0 +1,190 @@
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[(startIdx + 2)..endIdx];
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)
{
return string.IsNullOrEmpty(input) ? input : 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();
}
}
@@ -0,0 +1,61 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>YesNt.Interpreter</RootNamespace>
<ApplicationIcon />
<OutputType>Library</OutputType>
<StartupObject />
<Platforms>AnyCPU;x64</Platforms>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<PackageReadmeFile>README.md</PackageReadmeFile>
<RepositoryUrl>https://github.com/Stone-Red-Code/YesNt-Interpreter/</RepositoryUrl>
<PackageTags>scripting, modding, language</PackageTags>
<PackageIcon>Logo.png</PackageIcon>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<IsAotCompatible>True</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\assets\Logo.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\..\LICENSE">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\..\README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.20.0.135146">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\YesNt.Interpreter.Generator\YesNt.Interpreter.Generator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>