Add XML docs

This commit is contained in:
Stone_Red
2026-03-04 20:44:06 +01:00
parent ac78afc12f
commit a2f621d3f4
16 changed files with 347 additions and 0 deletions
@@ -4,19 +4,69 @@ 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 colour 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>
/// Initialises a new <see cref="StatementAttribute"/> with a syntax-highlight colour.
/// </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 colour used for syntax highlighting in the code editor.</param>
public StatementAttribute(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor color)
{
Name = name;
@@ -25,6 +75,13 @@ public class StatementAttribute : Attribute
Color = color;
}
/// <summary>
/// Initialises a new <see cref="StatementAttribute"/> without a syntax-highlight colour.
/// 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;
@@ -4,9 +4,28 @@ 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;
}
+16
View File
@@ -1,12 +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
}
+10
View File
@@ -1,9 +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
}
+10
View File
@@ -1,9 +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
}
@@ -2,11 +2,32 @@
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; }
}
@@ -1,5 +1,9 @@
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";
@@ -2,12 +2,27 @@
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();
}
+6
View File
@@ -1,10 +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;
}
@@ -5,6 +5,13 @@ 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
{
public event Action<string> OnDebugOutput;
@@ -4,12 +4,32 @@ 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 colour 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; }
}
@@ -1,6 +1,16 @@
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; }
}
@@ -10,16 +10,45 @@ using YesNt.Interpreter.Utilities;
namespace YesNt.Interpreter.Runtime;
/// <summary>
/// The main entry point for executing YesNt scripts.
/// </summary>
/// <example>
/// Running a script file:
/// <code>
/// var interpreter = new YesNtInterpreter();
/// interpreter.Execute("path/to/script.ynt");
/// </code>
/// Running script lines in memory with a custom statement:
/// <code>
/// var interpreter = new YesNtInterpreter();
/// interpreter.AddStatement("log", SearchMode.StartOfLine, SpaceAround.End, args =>
/// Console.WriteLine($"[LOG] {args}"));
/// interpreter.Execute(new List&lt;string&gt; { "log hello world" });
/// </code>
/// </example>
public class YesNtInterpreter
{
/// <summary>
/// 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 readonly List<KeyValuePair<StaticStatementAttribute, Action>> staticStatements;
/// <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
@@ -41,6 +70,16 @@ public class YesNtInterpreter
}
}
/// <summary>
/// Registers a custom statement using a pre-built <see cref="StatementAttribute"/>.
/// If a statement with the same attribute key already exists it will be replaced.
/// 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;
@@ -50,16 +89,34 @@ public class YesNtInterpreter
.ToDictionary(x => x.Key, x => x.Value);
}
/// <summary>
/// Registers a custom statement without a syntax-highlight colour.
/// </summary>
/// <param name="name">The keyword that identifies this statement in source code.</param>
/// <param name="searchMode">Where in the line the keyword is matched.</param>
/// <param name="spaceAround">Which sides of the keyword require a surrounding space.</param>
/// <param name="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 custom statement with a syntax-highlight colour.
/// </summary>
/// <param name="name">The keyword that identifies this statement in source code.</param>
/// <param name="searchMode">Where in the line the keyword is matched.</param>
/// <param name="spaceAround">Which sides of the keyword require a surrounding space.</param>
/// <param name="consoleColor">The colour used for syntax highlighting in the code editor.</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>
/// Initialises a new <see cref="YesNtInterpreter"/> and registers all built-in statements.
/// </summary>
public YesNtInterpreter()
{
GeneratedStatementRegistry.Register(runtimeInfo, out statements, out staticStatements);
@@ -68,11 +125,23 @@ public class YesNtInterpreter
runtimeInfo.OnLineExecuted += e => OnLineExecuted?.Invoke(e);
}
/// <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();
@@ -83,6 +152,14 @@ public class YesNtInterpreter
}
}
/// <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();
+18
View File
@@ -4,8 +4,19 @@ 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 recognised condition form (treated as an error by callers).
/// </returns>
public static bool? EvaluateCondition(string input)
{
if (input.ToLower().FromSafeString().Trim() == "true")
@@ -68,6 +79,13 @@ internal static partial class Evaluator
return null;
}
/// <summary>
/// Evaluates a numeric arithmetic expression string and returns the result as a string.
/// Supports <c>+</c>, <c>-</c>, <c>*</c>, and <c>/</c> operators.
/// Adjacent sign characters (<c>++</c>, <c>--</c>, <c>-+</c>, <c>+-</c>) are normalised 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 = PlusPlusRegex().Replace(input, "+");
@@ -11,6 +11,14 @@ public delegate void DataReceivedEventHandler(object sender, DataReceivedEventAr
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>
public class FixedProcess : Process
{
public new event DataReceivedEventHandler OutputDataReceived;
@@ -6,10 +6,27 @@ 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 tilde-prefixed three-letter codes
/// (e.g. space → <c>~spc</c>, newline → <c>~nli</c>). 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
{
private static readonly Dictionary<string, string> reverseReplacementRules;
/// <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()
{
{"~", "~til" },
@@ -34,6 +51,13 @@ public static class StringExtensions
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)
{
StringBuilder output = new StringBuilder();
@@ -45,28 +69,53 @@ public static class StringExtensions
return ReplaceOnce(output.ToString(), ReplacementRules);
}
/// <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)
{
return ReplaceOnce(input.Replace("\v", string.Empty), reverseReplacementRules);
}
/// <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)
{
return double.TryParse(input.FromSafeString().Replace(',', '.'), 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;