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. /// /// /// Running a script file: /// /// var interpreter = new YesNtInterpreter(); /// interpreter.Execute("path/to/script.ynt"); /// /// Running script lines in memory with a custom statement: /// /// var interpreter = new YesNtInterpreter(); /// interpreter.AddStatement("log", SearchMode.StartOfLine, SpaceAround.End, args => /// Console.WriteLine($"[LOG] {args}")); /// interpreter.Execute(new List<string> { "log hello world" }); /// /// 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 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); runtimeInfo.OnDebugOutput += (s) => OnDebugOutput?.Invoke(s); runtimeInfo.OnLineExecuted += e => OnLineExecuted?.Invoke(e); } /// /// 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); } /// /// Registers a custom statement without a syntax-highlight color. /// /// The keyword that identifies this statement in source code. /// Where in the line the keyword is matched. /// Which sides of the keyword require a surrounding 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 custom statement with a syntax-highlight color. /// /// The keyword that identifies this statement in source code. /// Where in the line the keyword is matched. /// Which sides of the keyword require a surrounding space. /// The color used for syntax highlighting in the code editor. /// 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); } /// /// Permanently removes all built-in or custom statements that match . /// After removal, any script line that would have matched triggers an "Invalid statement" error. /// /// The keyword of the statement(s) 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); } /// /// 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] = _ => { }; } } /// /// 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); } /// /// 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++) { runtimeInfo.Lines.Add(new Line(lines[i], Path.GetFileName("#Memory#"), i)); } 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; } Execute(); } private void Execute() { for (; runtimeInfo.LineNumber < runtimeInfo.Lines.Count; runtimeInfo.LineNumber++) { if (runtimeInfo.Stop) { break; } runtimeInfo.CurrentLine = runtimeInfo.Lines[runtimeInfo.LineNumber].Content.Trim(' ').Replace("\r", string.Empty); if (string.IsNullOrWhiteSpace(runtimeInfo.CurrentLine) || runtimeInfo.CurrentLine.StartsWith('#')) { continue; } DebugEventArgs 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; foreach (KeyValuePair> statement in statements) { StatementAttribute statementAttribute = statement.Key; if (!statementAttribute.ExecuteInSearchMode && runtimeInfo.IsSearching) { statementFound = true; continue; } if (runtimeInfo.Stop) { break; } string name = statementAttribute.SpaceAround switch { SpaceAround.StartEnd => $" {statementAttribute.Name.Trim()} ", SpaceAround.Start => $" {statementAttribute.Name.Trim()}", SpaceAround.End => $"{statementAttribute.Name.Trim()} ", _ => statementAttribute.Name.Trim() }; if (statementAttribute.Separator is null || runtimeInfo.CurrentLine.Contains(statementAttribute.Separator)) { if (statementAttribute.SearchMode == SearchMode.StartOfLine && runtimeInfo.CurrentLine.StartsWith(name)) { string copyLine = statementAttribute.KeepStatementInArgs ? runtimeInfo.CurrentLine : runtimeInfo.CurrentLine[name.Length..]; statement.Value.Invoke(copyLine); statementFound = true; } else if (statementAttribute.SearchMode == SearchMode.Contains && runtimeInfo.CurrentLine.Contains(name)) { string copyLine = statementAttribute.KeepStatementInArgs ? runtimeInfo.CurrentLine : runtimeInfo.CurrentLine.Replace(name, string.Empty); statement.Value.Invoke(copyLine); statementFound = true; } else if (statementAttribute.SearchMode == SearchMode.EndOfLine && runtimeInfo.CurrentLine.EndsWith(name)) { string copyLine = statementAttribute.KeepStatementInArgs ? runtimeInfo.CurrentLine : runtimeInfo.CurrentLine[..^name.Length]; statement.Value.Invoke(copyLine); statementFound = true; } else if (statementAttribute.SearchMode == SearchMode.Exact && runtimeInfo.CurrentLine.Equals(name)) { statement.Value.Invoke(runtimeInfo.CurrentLine); statementFound = true; } } } if (!statementFound) { runtimeInfo.Exit(ExitMessages.InvalidStatement, true); } if (runtimeInfo.IsDebugMode && notSearchingLabel) { 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)) { Console.WriteLine($"File \"{path}\" not found!"); return false; } string[] lines = File.ReadAllLines(path); if (lines.Length <= 0) { Console.WriteLine($"File \"{path}\" is empty!"); return false; } runtimeInfo.WorkingDirectory = Path.GetDirectoryName(path); for (int i = 0; i < lines.Length; i++) { runtimeInfo.Lines.Add(new Line(lines[i], Path.GetFileName(path), i)); } return true; } }