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; /// /// 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 already exists it will be replaced. /// 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 colour. /// /// 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 colour. /// /// 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 colour 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); } /// /// 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; } }