using System; using System.Collections.Generic; using System.Threading; using YesNt.Interpreter.Utilities; namespace YesNt.Interpreter.Runtime; /// /// 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 task statement owns its own /// whose points back /// to the main execution context. /// internal sealed class RuntimeInformation : IStatementContext { public event Action OnDebugOutput; public event Action OnLineExecuted; private event Action OnExit; private static int internalTaskId = 0; private readonly Dictionary topVariables = []; private readonly Dictionary> topLists = []; public Dictionary GlobalVariables { get; set; } = []; public Dictionary Functions { get; } = []; public Dictionary BlockBoundaries { get; } = []; internal Action PreScanLinesAction { get; set; } public Stack FunctionCallStack { get; } = new(); public Stack InParametersStack { get; } = new(); public Stack OutParametersStack { get; set; } = new(); public List Lines { get; set; } = []; public string CurrentLine { get; set; } = string.Empty; public string SearchLabel { get; set; } = string.Empty; public string SearchFunction { get; set; } = string.Empty; public int LineNumber { get; set; } = 0; public bool Stop { get; private set; } = false; public bool StopAllTasks { get; private set; } = false; public bool IsDebugMode { get; set; } = false; public string WorkingDirectory { get; set; } = string.Empty; public bool IsTask => ParentRuntimeInformation is not null; public int TaskId { get => IsTask ? field : 0; private set; } = 0; public bool InternalIsInFunction { get; set; } public bool IsInFunction { get => InternalIsInFunction || FunctionCallStack.Count > 0; set => InternalIsInFunction = value; } public Dictionary Variables => FunctionCallStack.Count == 0 ? topVariables : FunctionCallStack.Peek().Variables; public Dictionary> Lists => FunctionCallStack.Count == 0 ? topLists : FunctionCallStack.Peek().Lists; public Dictionary Labels { get => FunctionCallStack.Count == 0 ? field : FunctionCallStack.Peek().Labels; } = []; public RuntimeInformation ParentRuntimeInformation { get; set { field = value; field?.OnExit += ParentRuntimeInformation_OnExit; } } public bool IsSearching => !string.IsNullOrWhiteSpace(SearchLabel + SearchFunction) || (IsInFunction && FunctionCallStack.Count == 0); public bool IsLocalSearch { get; set; } public void WriteLine(string output, bool forceWrite = false) { if ((Stop && !forceWrite) || (ParentRuntimeInformation?.StopAllTasks == true && !forceWrite)) { return; } if (IsDebugMode) { if (IsTask) { ParentRuntimeInformation!.WriteLine(output.FromSafeString(), forceWrite); } else { OnDebugOutput?.Invoke(output.FromSafeString() + Environment.NewLine); } } else { Console.WriteLine(output.FromSafeString()); } } public void Write(string output, bool forceWrite = false) { if ((Stop && !forceWrite) || (ParentRuntimeInformation?.StopAllTasks == true && !forceWrite)) { return; } if (IsDebugMode) { if (IsTask) { ParentRuntimeInformation!.Write(output.FromSafeString(), forceWrite); } else { OnDebugOutput?.Invoke(output.FromSafeString()); } } else { Console.Write(output.FromSafeString()); } } public void Exit(string message, bool stopAllTasks) { if (!Stop) { if (Lines.Count == 0) { WriteLine($"{Environment.NewLine}[{(IsTask ? $"Task {TaskId}" : "The process")} was terminated with the message: {message}]", true); } else { Line line = Lines[Math.Min(LineNumber, Lines.Count - 1)]; WriteLine($"{Environment.NewLine}[{(IsTask ? $"Task {TaskId}" : "The process")} was terminated at line {line.LineNumber + 1} in the file \"{line.FileName}\" with the message: {message}]", true); } while (FunctionCallStack.Count > 0) { int stackLineNumber = FunctionCallStack.Pop().CallerLine; List targetLines = ParentRuntimeInformation?.Lines ?? Lines; if (stackLineNumber >= 0 && stackLineNumber < targetLines.Count) { Line stackLine = targetLines[stackLineNumber]; WriteLine($" at line {stackLine.LineNumber + 1} in the file \"{stackLine.FileName}\"", true); } else { WriteLine(" at unknown location (source not available)", true); } } Stop = true; } if (stopAllTasks && !StopAllTasks) { StopAllTasks = true; OnExit?.Invoke(message, StopAllTasks); ParentRuntimeInformation?.Exit(ExitMessages.TerminatedByChildTask, true); } } public void LineExecuted(DebugEventArgs debugEventArgs) { if (IsTask) { ParentRuntimeInformation.LineExecuted(debugEventArgs); } else { OnLineExecuted?.Invoke(debugEventArgs); } } public void Reset() { topVariables.Clear(); topLists.Clear(); Lines.Clear(); GlobalVariables.Clear(); Labels.Clear(); Functions.Clear(); BlockBoundaries.Clear(); FunctionCallStack.Clear(); InParametersStack.Clear(); OutParametersStack.Clear(); ParentRuntimeInformation = null; SearchLabel = string.Empty; SearchFunction = string.Empty; WorkingDirectory = string.Empty; CurrentLine = string.Empty; Stop = false; StopAllTasks = false; IsDebugMode = false; IsInFunction = false; IsLocalSearch = false; LineNumber = 0; #pragma warning disable S2696 // internalTaskId is a shared counter intentionally incremented by each Reset call TaskId = Interlocked.Increment(ref internalTaskId); #pragma warning restore S2696 } private void ParentRuntimeInformation_OnExit(string exitMessage, bool stopAllTasks) { Exit(ExitMessages.TerminatedByParentTask, stopAllTasks); } }