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 }; } }