mirror of
https://github.com/Stone-Red-Code/YesNt-Interpreter.git
synced 2026-09-08 16:06:07 +02:00
Add snapcraft & chocolatey manifests and move code to src
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user