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;
///
/// The main entry point for executing YesNt scripts.
///
public class YesNtInterpreter
{
///
/// Raised after each line is executed in debug mode. The argument is
/// when execution ends (either normally or due to an error), allowing callers to detect completion.
///
public event Action OnLineExecuted;
///
/// Raised in debug mode whenever the script produces output (e.g. via print_line).
/// In non-debug mode output is written directly to .
///
public event Action OnDebugOutput;
private readonly RuntimeInformation runtimeInfo = new RuntimeInformation();
private Dictionary> statements;
private List statementHandlers;
private List> lineMatchingHandlers = [];
private readonly List> staticStatements;
private readonly Dictionary>>> disabledStatements = [];
///
/// Gets a read-only snapshot of all currently registered statements.
/// Useful for building syntax highlighters or documentation tools.
///
public ReadOnlyCollection StatementInformation
{
get
{
List 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(information);
}
}
///
/// Initializes a new and registers all built-in statements.
///
public YesNtInterpreter()
{
GeneratedStatementRegistry.Register(runtimeInfo, out statements, out staticStatements);
UpdateStatementHandlers();
runtimeInfo.PreScanLinesAction = PreScanLines;
runtimeInfo.OnDebugOutput += (s) => OnDebugOutput?.Invoke(s);
runtimeInfo.OnLineExecuted += e => OnLineExecuted?.Invoke(e);
}
private void UpdateStatementHandlers()
{
statementHandlers = statements.Select(s =>
{
string name = s.Key.SpaceAround switch
{
SpaceAround.StartEnd => $" {s.Key.Name.Trim()} ",
SpaceAround.Start => $" {s.Key.Name.Trim()}",
SpaceAround.End => $"{s.Key.Name.Trim()} ",
_ => s.Key.Name.Trim()
};
return new StatementHandler(s.Key, s.Value, name);
}).ToList();
}
///
/// Registers a custom statement using a pre-built .
/// If a statement with the same attribute key (identical field values) already exists it will be replaced;
/// otherwise a new entry is added. Built-in statements use distinct attribute instances, so passing a
/// newly constructed attribute with the same name will add a second handler rather than replacing
/// the built-in. Use first to replace a built-in keyword.
/// The statement list is re-sorted by priority after insertion.
///
/// The attribute describing the keyword, search mode, and priority.
///
/// The delegate invoked when the statement matches. Receives the argument text
/// (the part of the line after the keyword, unless is set).
///
public void AddStatement(StatementAttribute attribute, Action 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();
}
///
/// Registers a custom statement using a pre-built ,
/// with access to the script's (variables, line number, output, etc.).
///
/// The attribute describing the keyword, search mode, and priority.
///
/// The delegate invoked when the statement matches. Receives the argument text and the current
/// for reading/writing script state.
///
public void AddStatement(StatementAttribute attribute, Action handler)
{
AddStatement(attribute, args => handler(args, runtimeInfo));
}
///
/// Registers a simple custom statement with default settings.
///
/// The keyword to match.
/// Where in the line the keyword is searched for.
/// Which sides of the keyword must be padded with a space.
/// The delegate invoked when the statement matches.
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action handler)
{
AddStatement(new StatementAttribute(name, searchMode, spaceAround), handler);
}
///
/// Registers a simple custom statement with default settings,
/// with access to the script's (variables, line number, output, etc.).
///
/// The keyword to match.
/// Where in the line the keyword is searched for.
/// Which sides of the keyword must be padded with a space.
///
/// The delegate invoked when the statement matches. Receives the argument text and the current
/// for reading/writing script state.
///
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action handler)
{
AddStatement(new StatementAttribute(name, searchMode, spaceAround), handler);
}
///
/// Registers a simple custom statement with a specific syntax-highlight color.
///
/// The keyword to match.
/// Where in the line the keyword is searched for.
/// Which sides of the keyword must be padded with a space.
/// The color used for syntax highlighting.
/// The delegate invoked when the statement matches.
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor consoleColor, Action handler)
{
AddStatement(new StatementAttribute(name, searchMode, spaceAround, consoleColor), handler);
}
///
/// Registers a simple custom statement with a specific syntax-highlight color,
/// with access to the script's (variables, line number, output, etc.).
///
/// The keyword to match.
/// Where in the line the keyword is searched for.
/// Which sides of the keyword must be padded with a space.
/// The color used for syntax highlighting.
///
/// The delegate invoked when the statement matches. Receives the argument text and the current
/// for reading/writing script state.
///
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor consoleColor, Action handler)
{
AddStatement(new StatementAttribute(name, searchMode, spaceAround, consoleColor), handler);
}
///
/// Unregisters all handlers matching the specified keyword .
///
/// The keyword to remove.
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();
}
///
/// Disables all statements matching 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 to restore original behavior.
///
/// The keyword of the statement(s) to disable.
public void DisableStatement(string name)
{
if (disabledStatements.ContainsKey(name))
{
return;
}
List>> matching =
statements.Where(kv => kv.Key.Name == name).ToList();
if (matching.Count == 0)
{
return;
}
disabledStatements[name] = matching;
foreach (KeyValuePair> kv in matching)
{
statements[kv.Key] = _ => { };
}
UpdateStatementHandlers();
PreScanLines();
}
///
/// Re-enables statements previously disabled with ,
/// restoring their original handlers.
/// Has no effect if the statement is not currently disabled.
///
/// The keyword of the statement(s) to re-enable.
public void EnableStatement(string name)
{
if (!disabledStatements.TryGetValue(name, out List>> saved))
{
return;
}
foreach (KeyValuePair> kv in saved)
{
statements[kv.Key] = kv.Value;
}
_ = disabledStatements.Remove(name);
UpdateStatementHandlers();
PreScanLines();
}
///
/// Requests a graceful stop of the currently executing script.
/// The interpreter will terminate at the next line boundary.
///
public void Stop()
{
runtimeInfo.Exit(ExitMessages.TerminatedByExternalProcess, true);
}
///
/// Executes a YesNt script file.
///
/// The path to the .ynt script file.
///
/// When , output is routed through instead of
/// and line-execution events are raised via .
///
public void Execute(string path, bool isDebugMode = false)
{
runtimeInfo.Reset();
runtimeInfo.IsDebugMode = isDebugMode;
if (LoadFile(path))
{
Execute();
}
}
///
/// Executes a YesNt script supplied as an in-memory list of lines.
///
/// The script lines to execute.
///
/// When , output is routed through and
/// line-execution events are raised via .
///
public void Execute(List 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 lines, Dictionary 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 staticStatement in staticStatements)
{
StaticStatementAttribute staticStatementAttribute = staticStatement.Key;
if (!staticStatementAttribute.ExecuteInSearchMode && runtimeInfo.IsSearching)
{
continue;
}
staticStatement.Value.Invoke();
}
bool statementFound = false;
bool notSearchingLabel = !runtimeInfo.IsSearching;
List 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>(runtimeInfo.Lines.Count);
// Dictionary to track open blocks by their expected end statement name
Dictionary> openBlocks = [];
for (int i = 0; i < runtimeInfo.Lines.Count; i++)
{
string content = runtimeInfo.Lines[i].Content;
List 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 stack))
{
stack = new Stack();
openBlocks[blockPair] = stack;
}
stack.Push(i);
}
// Track block ends
if (handler.Attribute.IsBlockEnd && openBlocks.TryGetValue(handler.Attribute.Name, out Stack 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 stack))
{
stack = new Stack();
openBlocks[intermediatePair] = stack;
}
if (stack.Count > 0)
{
int startLine = stack.Pop();
runtimeInfo.BlockBoundaries[startLine] = i;
}
stack.Push(i);
}
}
}
}
lineMatchingHandlers.Add(matchingHandlers);
}
}
private static bool IsPossibleMatch(string content, StatementHandler handler)
{
StatementAttribute attr = handler.Attribute;
string fullName = handler.FullName;
return attr.SearchMode switch
{
SearchMode.Exact => content == fullName,
SearchMode.StartOfLine => content.StartsWith(fullName, StringComparison.Ordinal),
SearchMode.EndOfLine => content.EndsWith(fullName, StringComparison.Ordinal),
SearchMode.Contains => content.Contains(fullName, StringComparison.Ordinal),
_ => false
};
}
}