diff --git a/.editorconfig b/.editorconfig index 051cab7..4b19b40 100644 --- a/.editorconfig +++ b/.editorconfig @@ -64,6 +64,9 @@ dotnet_naming_style.pascal_case.required_suffix = dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case +# S1172: Unused method parameters should be removed +dotnet_diagnostic.S1172.severity = none + [*.{cs,vb}] #### Naming styles #### @@ -111,3 +114,6 @@ dotnet_naming_style.pascal_case.required_prefix = dotnet_naming_style.pascal_case.required_suffix = dotnet_naming_style.pascal_case.word_separator = dotnet_naming_style.pascal_case.capitalization = pascal_case + +# IDE0305: Simplify collection initialization +dotnet_diagnostic.IDE0305.severity = none diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fd5b5d2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Stone_Red + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 4ed66ba..aa9b5e0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,57 @@ -# YesNt - -> YesNt is a simple imperative and interpreted language. +![YesNt-Interpreter](https://socialify.git.ci/Stone-Red-Code/YesNt-Interpreter/image?description=1&font=Inter&forks=1&issues=1&logo=https%3A%2F%2Fraw.githubusercontent.com%2FStone-Red-Code%2FYesNt-Interpreter%2Frefs%2Fheads%2Fdevelop%2Fassets%2FLogo.svg&name=1&pattern=Diagonal+Stripes&pulls=1&stargazers=1&theme=Auto) -Check out the [Wiki](https://github.com/Stone-Red-Code/YesNt-Interpreter/wiki) (Work in progress) +- [Documentation](docs/README.md) +- [Language Reference](docs/language-reference.md) +- [Releases](https://github.com/Stone-Red-Code/YesNt-Interpreter/releases) + +## What is it? + +YesNt is a line-based, interpreted scripting language. The core idea is simple: each line maps to one primary statement. Lines are processed top-to-bottom, and inline tokens (like `${variable}` or `%read_line`) are substituted before the statement executes. Postfix modifiers like `calc` and `task` can also appear at the end of a line to evaluate arithmetic or fork execution into a background thread. + +**Key characteristics:** + +- **Line-based execution:** Lines are processed top-to-bottom. Each line is a self-contained statement +- **Explicit control flow:** Labels, `goto`, and conditional jumps alongside structured `if`/`while` blocks +- **Explicit scoping:** `var` for function-local variables, `global` for cross-scope shared state +- **Functions with stacks:** Arguments and return values are passed via explicit push/pop stacks (`push_in`, `%in`, `push_out`, `%out`) +- **Background tasks:** Any line can be forked into a background execution flow with `task` +- **Embeddable:** The interpreter ships as a C# library. Custom statements can be registered, and built-in ones can be removed or disabled for sandboxing + +YesNt is intentionally minimal and is well suited for: + +- Scripting in games or applications where simple syntax and customizability are needed +- Rapid prototyping or automation tasks where a lightweight embedded language is beneficial +- Educational purposes to learn language design by adding new statements or modifying existing ones +- Embedding as a scripting engine in larger C# projects, with the ability to expose custom functionality through registered statements + +## Usage + +1. Download & install the latest release + - GitHub: [releases](https://github.com/Stone-Red-Code/YesNt-Interpreter/releases) +2. Run a script with the interpreter CLI + - `yesnt script.ynt` runs a script +3. Or use the terminal code editor + - `yesntcode` opens the editor (optional path to load a file) + - `run` / `debug` to execute the open script + - `format` to auto-format indentation + +## Example + +```ynt +func greet: + var name = %in + print_line Hello, ${name}! +return + +func add: + var result = %in + %in calc + push_out ${result} +return + +call greet with Alice +call greet with Bob + +call add with 3, 7 +var sum = %out +print_line 3 + 7 = ${sum} +``` diff --git a/SYNTAX_V2.md b/SYNTAX_V2.md new file mode 100644 index 0000000..6669145 --- /dev/null +++ b/SYNTAX_V2.md @@ -0,0 +1,116 @@ +# YesNt v2 Syntax + +This document describes the current, word-based YesNt syntax. + +## Goals + +- Keep current semantics and execution model. +- Replace terse mnemonics and symbols with readable words. +- Preserve line-based scripting. +- Keep the language line-based and lightweight. + +## v2 Core Rules + +- Statements are line-based. +- `# ...` remains a comment. +- Variable interpolation inside text uses `${name}`. +- String literals use double quotes (`"..."`) with escapes like `\n`, `\t`, `\"`, `\\`. + Interpolation is not evaluated inside string literals. +- Function and label declarations are block markers with a trailing `:`. +- Conditions retain current evaluator expressions (for example: `a == b`, `x > 5`, `10 + 2 == 12`). + +## Quick Example + +```ynt +var name = world +func greet: +print_line "Hello world" +print_line Hello ${name} +return +call greet +``` + +## Block Conditionals + +```ynt +if 10 > 5: +print_line yes +else: +print_line no +end_if +``` + +## While Loops + +```ynt +var i = 3 +while ${i} > 0: +print_line ${i} +var i = ${i} - 1 calc +end_while +``` + +## Lists + +```ynt +list items new +list items add apple +list items add banana +list items get 1 +var value = %out +print_line ${value} +``` + +## Full v1 -> v2 Mapping + +| Area | v1 Syntax | v2 Syntax | Notes | +|---|---|---|---| +| Variables | `x` | `${x}` | Variable read/interpolation token. | +| Console | `cwl` | `print_line` | Empty line print. | +| Console | `cwl text` | `print_line text` | Print with newline. | +| Console | `cw text` | `print text` | Print without newline. | +| Console | `%crl` | `%read_line` | Inline token, inserts user line input. | +| Console | `%cr` | `%read_key` | Inline token, inserts user key input. | +| Console | `cls` | `clear` | Clear console. | +| Code flow | `lbl name` | `label name:` | Label declaration/target. | +| Code flow | `jmp name` | `goto name` | Unconditional jump. | +| Code flow | `jif name \| cond` | `if cond goto name` | Conditional jump. | +| Functions | `fnc name` | `func name:` | Function declaration. | +| Functions | `cal name` | `call name` | Function call without args. | +| Functions | `cal name \| a,b,c` | `call name with a, b, c` | Function call with args. | +| Functions | `in value` | `push_in value` | Push in-arg onto input stack. | +| Functions | `%get` | `%in` | Inline token, pop current call input arg. | +| Functions | `%isi` | `%has_in` | Inline token, bool if input arg exists. | +| Functions | `put value` | `push_out value` | Push out-arg in function. | +| Functions | `%out` | `%out` | Keep token name for familiarity. | +| Functions | `%iso` | `%has_out` | Inline token, bool if out arg exists. | +| Functions | `ret` | `return` | Return from function. | +| Functions | `ccs` | `clear_call_stack` | Clear call stack. | +| Condition-call | `cif name \| cond` | `if cond call name` | Conditional function call. | +| Termination | `end` | `exit` | Planned termination. | +| Termination | `trm` | `abort_all` | Planned termination + cancel tasks. | +| Errors | `trw message` | `throw message` | Error termination. | +| Errors | `err message` | `error message` | Non-fatal/runtime message end state. | +| Processing | `expr !calc` | `expr calc` | Evaluate arithmetic fragments. | +| Processing | `text !eval` | `text eval` | Decode safe string literals. | +| Processing | `line !task` | `line task` | Run line in background task runtime. | +| Processing | `slp ms` | `sleep ms` | Sleep with runtime-aware cancellation. | +| Processing | `len text` | `length text` | Push text length to out stack. | +| Processing | `imp file` | `import file` | Inline include of another `.ynt` file. | +| System | `exc prog` | `exec prog` | Execute process with in-stack args. | +| System | `exc prog \| a,b,c` | `exec prog with a, b, c` | Execute process with explicit args. | +| Predefined | `%time` | `%time` | Unix timestamp token. | +| Predefined | `%os` | `%os` | OS platform token. | +| Predefined | `%cpu` | `%cpu` | Processor architecture token. | +| Predefined | `%is64` | `%is64` | 64-bit OS bool token. | +| Predefined | `%pi` | `%pi` | PI token. | +| Predefined | `%rnd` | `%rand` | Random number token. | + +## Notes + +- `%out` is intentionally kept as `%out`. +- Postfix operations are `calc`, `eval`, and `task`. + diff --git a/YesNt-Interpreter.sln b/YesNt-Interpreter.sln index ed6df67..8e36ce1 100644 --- a/YesNt-Interpreter.sln +++ b/YesNt-Interpreter.sln @@ -12,26 +12,82 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .editorconfig = .editorconfig EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YesNt.Interpreter.Tests", "YesNt.Interpreter.Tests\YesNt.Interpreter.Tests.csproj", "{2F95DCA7-3E43-4F2E-8FB2-067F0EC962B1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YesNt.Interpreter.Tests", "YesNt.Interpreter.Tests\YesNt.Interpreter.Tests.csproj", "{2F95DCA7-3E43-4F2E-8FB2-067F0EC962B1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YesNt.Interpreter.App", "YesNt.Interpreter.App\YesNt.Interpreter.App.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "YesNt.Interpreter.Generator", "YesNt.Interpreter.Generator\YesNt.Interpreter.Generator.csproj", "{85AAB233-9C4D-4B42-8117-00010958DD1D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {33BDA036-A37E-475E-AACF-8ED96E31BA1A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {33BDA036-A37E-475E-AACF-8ED96E31BA1A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {33BDA036-A37E-475E-AACF-8ED96E31BA1A}.Debug|x64.ActiveCfg = Debug|x64 + {33BDA036-A37E-475E-AACF-8ED96E31BA1A}.Debug|x64.Build.0 = Debug|x64 + {33BDA036-A37E-475E-AACF-8ED96E31BA1A}.Debug|x86.ActiveCfg = Debug|Any CPU + {33BDA036-A37E-475E-AACF-8ED96E31BA1A}.Debug|x86.Build.0 = Debug|Any CPU {33BDA036-A37E-475E-AACF-8ED96E31BA1A}.Release|Any CPU.ActiveCfg = Release|Any CPU {33BDA036-A37E-475E-AACF-8ED96E31BA1A}.Release|Any CPU.Build.0 = Release|Any CPU + {33BDA036-A37E-475E-AACF-8ED96E31BA1A}.Release|x64.ActiveCfg = Release|x64 + {33BDA036-A37E-475E-AACF-8ED96E31BA1A}.Release|x64.Build.0 = Release|x64 + {33BDA036-A37E-475E-AACF-8ED96E31BA1A}.Release|x86.ActiveCfg = Release|Any CPU + {33BDA036-A37E-475E-AACF-8ED96E31BA1A}.Release|x86.Build.0 = Release|Any CPU {E40573DB-912A-4871-BB65-3EE6E7D72E79}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E40573DB-912A-4871-BB65-3EE6E7D72E79}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E40573DB-912A-4871-BB65-3EE6E7D72E79}.Debug|x64.ActiveCfg = Debug|x64 + {E40573DB-912A-4871-BB65-3EE6E7D72E79}.Debug|x64.Build.0 = Debug|x64 + {E40573DB-912A-4871-BB65-3EE6E7D72E79}.Debug|x86.ActiveCfg = Debug|Any CPU + {E40573DB-912A-4871-BB65-3EE6E7D72E79}.Debug|x86.Build.0 = Debug|Any CPU {E40573DB-912A-4871-BB65-3EE6E7D72E79}.Release|Any CPU.ActiveCfg = Release|Any CPU {E40573DB-912A-4871-BB65-3EE6E7D72E79}.Release|Any CPU.Build.0 = Release|Any CPU + {E40573DB-912A-4871-BB65-3EE6E7D72E79}.Release|x64.ActiveCfg = Release|x64 + {E40573DB-912A-4871-BB65-3EE6E7D72E79}.Release|x64.Build.0 = Release|x64 + {E40573DB-912A-4871-BB65-3EE6E7D72E79}.Release|x86.ActiveCfg = Release|Any CPU + {E40573DB-912A-4871-BB65-3EE6E7D72E79}.Release|x86.Build.0 = Release|Any CPU {2F95DCA7-3E43-4F2E-8FB2-067F0EC962B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2F95DCA7-3E43-4F2E-8FB2-067F0EC962B1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F95DCA7-3E43-4F2E-8FB2-067F0EC962B1}.Debug|x64.ActiveCfg = Debug|x64 + {2F95DCA7-3E43-4F2E-8FB2-067F0EC962B1}.Debug|x64.Build.0 = Debug|x64 + {2F95DCA7-3E43-4F2E-8FB2-067F0EC962B1}.Debug|x86.ActiveCfg = Debug|Any CPU + {2F95DCA7-3E43-4F2E-8FB2-067F0EC962B1}.Debug|x86.Build.0 = Debug|Any CPU {2F95DCA7-3E43-4F2E-8FB2-067F0EC962B1}.Release|Any CPU.ActiveCfg = Release|Any CPU {2F95DCA7-3E43-4F2E-8FB2-067F0EC962B1}.Release|Any CPU.Build.0 = Release|Any CPU + {2F95DCA7-3E43-4F2E-8FB2-067F0EC962B1}.Release|x64.ActiveCfg = Release|x64 + {2F95DCA7-3E43-4F2E-8FB2-067F0EC962B1}.Release|x64.Build.0 = Release|x64 + {2F95DCA7-3E43-4F2E-8FB2-067F0EC962B1}.Release|x86.ActiveCfg = Release|Any CPU + {2F95DCA7-3E43-4F2E-8FB2-067F0EC962B1}.Release|x86.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|x64 + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU + {85AAB233-9C4D-4B42-8117-00010958DD1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85AAB233-9C4D-4B42-8117-00010958DD1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85AAB233-9C4D-4B42-8117-00010958DD1D}.Debug|x64.ActiveCfg = Debug|Any CPU + {85AAB233-9C4D-4B42-8117-00010958DD1D}.Debug|x64.Build.0 = Debug|Any CPU + {85AAB233-9C4D-4B42-8117-00010958DD1D}.Debug|x86.ActiveCfg = Debug|Any CPU + {85AAB233-9C4D-4B42-8117-00010958DD1D}.Debug|x86.Build.0 = Debug|Any CPU + {85AAB233-9C4D-4B42-8117-00010958DD1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85AAB233-9C4D-4B42-8117-00010958DD1D}.Release|Any CPU.Build.0 = Release|Any CPU + {85AAB233-9C4D-4B42-8117-00010958DD1D}.Release|x64.ActiveCfg = Release|Any CPU + {85AAB233-9C4D-4B42-8117-00010958DD1D}.Release|x64.Build.0 = Release|Any CPU + {85AAB233-9C4D-4B42-8117-00010958DD1D}.Release|x86.ActiveCfg = Release|Any CPU + {85AAB233-9C4D-4B42-8117-00010958DD1D}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/YesNt.CodeEditor/Editor.cs b/YesNt.CodeEditor/Editor.cs index dee06c7..25bdead 100644 --- a/YesNt.CodeEditor/Editor.cs +++ b/YesNt.CodeEditor/Editor.cs @@ -1,275 +1,402 @@ using System; using System.Collections.Generic; using System.IO; +using System.Timers; using YesNt.Interpreter.Runtime; -namespace YesNt.CodeEditor +namespace YesNt.CodeEditor; + +internal class TextEditor { - internal class TextEditor + private readonly InputHandler inputHandler; + private readonly SyntaxHighlighter syntaxHighlighter; + private readonly List debugOutput = []; + + private readonly Point oldSize = new Point(0, 0); + public YesNtInterpreter YesNtInterpreter { get; } = new(); + public int LineOffset { get; set; } = 0; + public List Lines { get; } = []; + public Point CursorPosition { get; } = new(0, 0); + public Mode EditMode { get; set; } = Mode.Command; + public string CurrentPath { get; set; } = string.Empty; + public bool IsStepDebugMode { get; set; } + + public TextEditor(string path) : this() { - private readonly InputHandler inputHandler; - private readonly SyntaxHighlighter syntaxHighlighter; - private readonly List debugOutput = new(); - - public YesNtInterpreter YesNtInterpreter { get; } = new(); - public int LineOffset { get; set; } = 0; - public List Lines { get; } = new(); - public Point CursorPosition { get; } = new(0, 0); - public Mode EditMode { get; set; } = Mode.Command; - public string CurrentPath { get; set; } = string.Empty; - - public TextEditor(string path) : this() + if (File.Exists(path)) { - if (File.Exists(path)) - { - Load(path); - } + _ = Load(path); } + } - public TextEditor() + public TextEditor() + { + YesNtInterpreter.OnDebugOutput += YesNtInterpreter_OnDebugOutput; + YesNtInterpreter.OnLineExecuted += YesNtInterpreter_OnLineExecuted; + syntaxHighlighter = new(YesNtInterpreter.StatementInformation); + inputHandler = new InputHandler(this); + Console.CancelKeyPress += Console_CancelKeyPress; + + Timer timer = new Timer(100); + timer.Elapsed += (s, e) => { - YesNtInterpreter.Initialize(); - YesNtInterpreter.OnDebugOutput += YesNtInterpreter_OnDebugOutput; - YesNtInterpreter.OnLineExecuted += YesNtInterpreter_OnLineExecuted; - syntaxHighlighter = new(YesNtInterpreter.StatementInformation); - inputHandler = new InputHandler(this); - Console.CancelKeyPress += Console_CancelKeyPress; - } - - public void Run() - { - Console.Clear(); - - Display(true); - do + if (SizeChanged() && EditMode != Mode.Debug) { - Display(false); - } while (inputHandler.HandleInput()); + Display(true); - Console.Clear(); - } - - public void Display(bool drawAll) - { - Console.CursorVisible = false; - Console.ForegroundColor = ConsoleColor.Gray; - Console.BackgroundColor = ConsoleColor.Black; - - Console.SetCursorPosition(0, 0); - - if (SizeChanged()) - { - drawAll = true; - InputHandler.WriteStatus(string.Empty); - } - - if (LineOffset < 0 || CursorPosition.Y < 0 || CursorPosition.Y < 0) - { - LineOffset = 0; - CursorPosition.Y = 0; - CursorPosition.X = 0; - } - - for (int i = LineOffset; i < Console.WindowHeight + LineOffset - 2; i++) - { - Console.SetCursorPosition(0, i - LineOffset); - - string lineCountString = $"{i + 1}".PadRight(GetSpacing(), ' ') + "| "; - if (i < Lines.Count) + if (EditMode == Mode.Command) { - if (CursorPosition.Y == i || drawAll) - { - Console.Write(lineCountString); - string printLine = $"{Lines[i][..Math.Min(Lines[i].Length, Console.WindowWidth)]}".TrimEnd(); - syntaxHighlighter.Write(printLine); - Console.Write(new string(' ', Math.Max(Console.WindowWidth - lineCountString.Length - printLine.Length, 0))); - } - } - else if (drawAll || CursorPosition.Y == i) - { - Console.Write(lineCountString + new string(' ', Console.WindowWidth - lineCountString.Length)); + InputHandler.WriteStatus(string.Empty); + Console.SetCursorPosition(3, Console.WindowHeight - 2); } } + }; + timer.Start(); + } - Console.SetCursorPosition(0, Console.WindowHeight - 3); - Console.Write(new string('-', Console.WindowWidth)); - Console.SetCursorPosition(0, Console.WindowHeight - 2); - Console.Write(">>>" + new string(' ', Console.WindowWidth - 3)); + public void Run() + { + Console.Clear(); - Console.SetCursorPosition(Math.Min(CursorPosition.X + GetSpacing() + 2, Console.WindowWidth - 1), CursorPosition.Y - LineOffset); + Display(true); + do + { + Display(false); + } while (inputHandler.HandleInput()); - Console.CursorVisible = true; + Console.Clear(); + } + + public void Display(bool drawAll) + { + Console.CursorVisible = false; + Console.ForegroundColor = ConsoleColor.Gray; + Console.BackgroundColor = ConsoleColor.Black; + + Console.SetCursorPosition(0, 0); + + if (SizeChanged()) + { + drawAll = true; + InputHandler.WriteStatus(string.Empty); } - public bool Load(string path) + if (LineOffset < 0 || CursorPosition.Y < 0 || CursorPosition.Y < 0) { + LineOffset = 0; + CursorPosition.Y = 0; + CursorPosition.X = 0; + } + + for (int i = LineOffset; i < Console.WindowHeight + LineOffset - 2; i++) + { + Console.SetCursorPosition(0, i - LineOffset); + + string lineCountString = $"{i + 1}".PadRight(GetSpacing(), ' ') + "| "; + if (i < Lines.Count) + { + if (CursorPosition.Y == i || drawAll) + { + Console.Write(lineCountString); + string printLine = $"{Lines[i][..Math.Min(Lines[i].Length, Console.WindowWidth)]}".TrimEnd(); + syntaxHighlighter.Write(printLine); + Console.Write(new string(' ', Math.Max(Console.WindowWidth - lineCountString.Length - printLine.Length, 0))); + } + } + else if (drawAll || CursorPosition.Y == i) + { + Console.Write(lineCountString + new string(' ', Console.WindowWidth - lineCountString.Length)); + } + } + + Console.SetCursorPosition(0, Console.WindowHeight - 3); + Console.Write(new string('-', Console.WindowWidth)); + Console.SetCursorPosition(0, Console.WindowHeight - 2); + Console.Write(">>>" + new string(' ', Console.WindowWidth - 3)); + + Console.SetCursorPosition(Math.Min(CursorPosition.X + GetSpacing() + 2, Console.WindowWidth - 1), Math.Min(CursorPosition.Y - LineOffset, Console.WindowHeight - 4)); + + Console.CursorVisible = true; + } + + public bool Load(string path) + { + if (string.IsNullOrEmpty(Path.GetExtension(path))) + { + path = Path.ChangeExtension(path, "ynt"); + } + + if (!File.Exists(path)) + { + InputHandler.WriteStatus("File does not exist!"); + return false; + } + + Lines.Clear(); + Lines.AddRange(File.ReadAllLines(path)); + CurrentPath = path; + InputHandler.WriteStatus("File Loaded!"); + return true; + } + + public bool Save(string input, bool loadIfExists) + { + string path; + if (input.Split(' ').Length == 2) + { + path = input.Split(' ')[1]; + + if (CurrentPath.Trim() != path.Trim() && loadIfExists) + { + return Load(path); + } if (string.IsNullOrEmpty(Path.GetExtension(path))) { path = Path.ChangeExtension(path, "ynt"); } - - if (!File.Exists(path)) - { - InputHandler.WriteStatus("File does not exist!"); - return false; - } - - Lines.Clear(); - Lines.AddRange(File.ReadAllLines(path)); CurrentPath = path; - InputHandler.WriteStatus("File Loaded!"); + } + else if (!string.IsNullOrWhiteSpace(CurrentPath)) + { + path = CurrentPath; + } + else if (input.Split(' ').Length > 2) + { + InputHandler.WriteStatus("Invalid arguments!"); + return false; + } + else + { + InputHandler.WriteStatus("File path is empty! (Save the file before you can use this command)"); + return false; + } + + if (string.IsNullOrEmpty(Path.GetExtension(path))) + { + path = Path.ChangeExtension(path, "ynt"); + } + + while (Lines.Count > 0 && string.IsNullOrWhiteSpace(Lines[^1])) + { + Lines.RemoveAt(Lines.Count - 1); + } + + try + { + File.WriteAllLines(path, Lines); + InputHandler.WriteStatus("File Saved!"); return true; } - - public bool Save(string input, bool loadIfExists) + catch (Exception ex) { - string path; - if (input.Split(' ').Length == 2) - { - path = input.Split(' ')[1]; - - if (CurrentPath.Trim() != path.Trim() && loadIfExists) - { - if (Load(path)) - { - return true; - } - else - { - return false; - } - } - if (string.IsNullOrEmpty(Path.GetExtension(path))) - { - path = Path.ChangeExtension(path, "ynt"); - } - CurrentPath = path; - } - else if (!string.IsNullOrWhiteSpace(CurrentPath)) - { - path = CurrentPath; - } - else if (input.Split(' ').Length > 2) - { - InputHandler.WriteStatus("Invalid arguments!"); - return false; - } - else - { - InputHandler.WriteStatus("File path is empty! (Save the file before you can use this command)"); - return false; - } - - if (string.IsNullOrEmpty(Path.GetExtension(path))) - { - path = Path.ChangeExtension(path, "ynt"); - } - - while (Lines.Count > 0 && string.IsNullOrWhiteSpace(Lines[^1])) - { - Lines.RemoveAt(Lines.Count - 1); - } - - try - { - File.WriteAllLines(path, Lines); - InputHandler.WriteStatus("File Saved!"); - return true; - } - catch (Exception ex) - { - InputHandler.WriteStatus(ex.Message); - return false; - } - } - - private void YesNtInterpreter_OnDebugOutput(string output) - { - debugOutput.Add(output); - } - - private void YesNtInterpreter_OnLineExecuted(Interpreter.Runtime.DebugEventArgs e) - { - lock (Console.Out) - { - if (e is not null) - { - Console.ForegroundColor = ConsoleColor.Magenta; - - string sharedString = (Console.CursorLeft != 0) ? Environment.NewLine : string.Empty; - sharedString += (e.IsTask ? $"[Task: {e.TaskId}]" : string.Empty) + $"[{e.LineNumber}]"; - - if (e.OriginalLine == e.CurrentLine) - { - Console.WriteLine($"{sharedString}[{e.CurrentLine}] ==>"); - } - else - { - Console.WriteLine($"{sharedString}[{e.OriginalLine}] => [{e.CurrentLine}] ==>"); - } - Console.ForegroundColor = ConsoleColor.Gray; - } - string[] outputs = debugOutput.ToArray(); - debugOutput.Clear(); - - foreach (string output in outputs) - { - Console.Write(output); - } - } - } - - private void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e) - { - e.Cancel = true; - switch (EditMode) - { - case Mode.Debug: - YesNtInterpreter.Stop(); - EditMode = Mode.Command; - break; - } - } - - public int GetSpacing() - { - int padding = (Console.WindowHeight + LineOffset - 3).ToString().Length; - padding = Math.Max(padding, Lines.Count.ToString().Length); - padding += 1; - return padding; - } - - private readonly Point oldSize = new Point(0, 0); - - private bool SizeChanged() - { - if (oldSize.X != Console.WindowWidth || oldSize.Y != Console.WindowHeight) - { - oldSize.X = Console.WindowWidth; - oldSize.Y = Console.WindowHeight; - return true; - } + InputHandler.WriteStatus(ex.Message); return false; } } - internal class Point + public int GetSpacing() { - public int X { get; set; } - public int Y { get; set; } + int padding = (Console.WindowHeight + LineOffset - 3).ToString().Length; + padding = Math.Max(padding, Lines.Count.ToString().Length); + padding += 1; + return padding; + } - public Point(int x, int y) + public void FormatLines() + { + const int indentationSize = 4; + List blockStack = []; + + for (int i = 0; i < Lines.Count; i++) { - X = x; - Y = y; + string trimmed = Lines[i].Trim(' '); + if (string.IsNullOrWhiteSpace(trimmed)) + { + Lines[i] = string.Empty; + continue; + } + + if (trimmed.StartsWith('#')) + { + Lines[i] = trimmed; + continue; + } + + if (trimmed == "else:") + { + for (int j = blockStack.Count - 1; j >= 0; j--) + { + if (blockStack[j] is "if" or "else") + { + blockStack.RemoveAt(j); + break; + } + } + } + bool isTerminatingStatement = trimmed == "exit" + || trimmed.StartsWith("throw ", StringComparison.Ordinal) + || trimmed.StartsWith("error ", StringComparison.Ordinal); + bool closesFunctionBlock = isTerminatingStatement && blockStack.Count > 0 && blockStack[^1] == "func"; + + if (trimmed == "end_if") + { + for (int j = blockStack.Count - 1; j >= 0; j--) + { + if (blockStack[j] is "if" or "else") + { + blockStack.RemoveAt(j); + break; + } + } + } + else if (trimmed == "end_while") + { + for (int j = blockStack.Count - 1; j >= 0; j--) + { + if (blockStack[j] == "while") + { + blockStack.RemoveAt(j); + break; + } + } + } + else if (trimmed == "return") + { + for (int j = blockStack.Count - 1; j >= 0; j--) + { + if (blockStack[j] == "func") + { + blockStack.RemoveAt(j); + break; + } + } + } + + int lineIndentation = closesFunctionBlock ? Math.Max(0, blockStack.Count - 1) : blockStack.Count; + Lines[i] = new string(' ', lineIndentation * indentationSize) + trimmed; + + if (!isTerminatingStatement && ( + (trimmed.StartsWith("if ", StringComparison.Ordinal) && trimmed.EndsWith(':')) + || (trimmed.StartsWith("while ", StringComparison.Ordinal) && trimmed.EndsWith(':')) + || (trimmed.StartsWith("func ", StringComparison.Ordinal) && trimmed.EndsWith(':')) + || trimmed == "else:")) + { + if (trimmed.StartsWith("if ", StringComparison.Ordinal)) + { + blockStack.Add("if"); + } + else if (trimmed.StartsWith("while ", StringComparison.Ordinal)) + { + blockStack.Add("while"); + } + else if (trimmed.StartsWith("func ", StringComparison.Ordinal)) + { + blockStack.Add("func"); + } + else + { + blockStack.Add("else"); + } + } + + if (isTerminatingStatement) + { + while (blockStack.Count > 0 && blockStack[^1] != "func") + { + blockStack.RemoveAt(blockStack.Count - 1); + } + + if (closesFunctionBlock && blockStack.Count > 0 && blockStack[^1] == "func") + { + blockStack.RemoveAt(blockStack.Count - 1); + } + } } } - internal enum Mode + private static string ToLiteral(string input) { - Edit, - Command, - Debug + return Microsoft.CodeAnalysis.CSharp.SymbolDisplay.FormatLiteral(input, false); } + + private void YesNtInterpreter_OnDebugOutput(string output) + { + debugOutput.Add(output); + } + + private void YesNtInterpreter_OnLineExecuted(DebugEventArgs e) + { + lock (Console.Out) + { + if (e is not null) + { + Console.ForegroundColor = ConsoleColor.Magenta; + + string sharedString = (Console.CursorLeft != 0) ? Environment.NewLine : string.Empty; + sharedString += (e.IsTask ? $"[Task: {e.TaskId}]" : string.Empty) + $"[{e.LineNumber}]"; + + if (e.OriginalLine == e.CurrentLine) + { + Console.WriteLine($"{sharedString}[{ToLiteral(e.CurrentLine)}] ==>"); + } + else + { + Console.WriteLine($"{sharedString}[{ToLiteral(e.OriginalLine)}] => [{ToLiteral(e.CurrentLine)}] ==>"); + } + Console.ForegroundColor = ConsoleColor.Gray; + } + string[] outputs = debugOutput.ToArray(); + debugOutput.Clear(); + + foreach (string output in outputs) + { + Console.Write(output); + } + + if (IsStepDebugMode && e is not null && !e.IsTask) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine("[Step] Press any key for next line (Ctrl+C to stop)..."); + Console.ForegroundColor = ConsoleColor.Gray; + _ = Console.ReadKey(true); + } + } + } + + private void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e) + { + e.Cancel = true; + switch (EditMode) + { + case Mode.Debug: + YesNtInterpreter.Stop(); + EditMode = Mode.Command; + break; + } + } + + private bool SizeChanged() + { + if (oldSize.X != Console.WindowWidth || oldSize.Y != Console.WindowHeight) + { + oldSize.X = Console.WindowWidth; + oldSize.Y = Console.WindowHeight; + return true; + } + return false; + } +} + +internal class Point(int x, int y) +{ + public int X { get; set; } = x; + public int Y { get; set; } = y; +} + +internal enum Mode +{ + Edit, + Command, + Debug } \ No newline at end of file diff --git a/YesNt.CodeEditor/GlobalSuppressions.cs b/YesNt.CodeEditor/GlobalSuppressions.cs index 3e5f094..05f5be2 100644 --- a/YesNt.CodeEditor/GlobalSuppressions.cs +++ b/YesNt.CodeEditor/GlobalSuppressions.cs @@ -5,4 +5,4 @@ using System.Diagnostics.CodeAnalysis; -[assembly: SuppressMessage("Critical Code Smell", "S3998:Threads should not lock on objects with weak identity", Justification = "", Scope = "member", Target = "~M:YesNt.CodeEditor.TextEditor.YesNtInterpreter_OnLineExecuted(YesNt.Interpreter.Runtime.DebugEventArgs)")] +[assembly: SuppressMessage("Critical Code Smell", "S3998:Threads should not lock on objects with weak identity", Justification = "", Scope = "member", Target = "~M:YesNt.CodeEditor.TextEditor.YesNtInterpreter_OnLineExecuted(YesNt.Interpreter.Runtime.DebugEventArgs)")] \ No newline at end of file diff --git a/YesNt.CodeEditor/InputHandler.cs b/YesNt.CodeEditor/InputHandler.cs index 59a3024..585e96a 100644 --- a/YesNt.CodeEditor/InputHandler.cs +++ b/YesNt.CodeEditor/InputHandler.cs @@ -1,320 +1,390 @@ using System; using System.Text; -namespace YesNt.CodeEditor +namespace YesNt.CodeEditor; + +internal class InputHandler(TextEditor textEditor) { - internal class InputHandler + private readonly TextEditor textEditor = textEditor; + + public bool HandleInput() { - private readonly TextEditor textEditor; - - public InputHandler(TextEditor textEditor) + while (Console.KeyAvailable) { - this.textEditor = textEditor; + _ = Console.ReadKey(true); } - - public bool HandleInput() + if (textEditor.EditMode == Mode.Edit) { - while (Console.KeyAvailable) - { - Console.ReadKey(true); - } - if (textEditor.EditMode == Mode.Edit) - { - ConsoleKeyInfo keyInfo = Console.ReadKey(true); + ConsoleKeyInfo keyInfo = Console.ReadKey(true); - if ((ConsoleModifiers.Alt & keyInfo.Modifiers) == ConsoleModifiers.Alt) + if ((ConsoleModifiers.Alt & keyInfo.Modifiers) == ConsoleModifiers.Alt) + { + switch (keyInfo.Key) { - switch (keyInfo.Key) - { - case ConsoleKey.C: - textEditor.EditMode = Mode.Command; - return true; + case ConsoleKey.C: + textEditor.EditMode = Mode.Command; + return true; - case ConsoleKey.B: - int position = textEditor.Lines.Count; - textEditor.CursorPosition.Y = position - 1; - textEditor.LineOffset = Math.Max(position - Console.WindowHeight + 3, 0); + case ConsoleKey.B: + int position = textEditor.Lines.Count; + textEditor.CursorPosition.Y = position - 1; + textEditor.LineOffset = Math.Max(position - Console.WindowHeight + 3, 0); - textEditor.Display(true); - return true; + textEditor.Display(true); + return true; - case ConsoleKey.T: - textEditor.CursorPosition.Y = 0; - textEditor.LineOffset = 0; + case ConsoleKey.T: + textEditor.CursorPosition.Y = 0; + textEditor.LineOffset = 0; - textEditor.Display(true); - return true; + textEditor.Display(true); + return true; - case ConsoleKey.S: - textEditor.CursorPosition.X = 0; - return true; + case ConsoleKey.S: + textEditor.CursorPosition.X = 0; + return true; - case ConsoleKey.E: - if (textEditor.Lines.Count > textEditor.CursorPosition.Y) - { - textEditor.CursorPosition.X = textEditor.Lines[textEditor.CursorPosition.Y].TrimEnd().Length; - } - else - { - textEditor.CursorPosition.X = 0; - } - return true; - } + case ConsoleKey.E: + textEditor.CursorPosition.X = textEditor.Lines.Count > textEditor.CursorPosition.Y ? textEditor.Lines[textEditor.CursorPosition.Y].TrimEnd().Length : 0; + return true; + + case ConsoleKey.R: + ExecuteWithDebugScreen("run", false, false); + textEditor.Display(true); + return true; + + case ConsoleKey.D: + ExecuteWithDebugScreen("debug", true, false); + textEditor.Display(true); + return true; + + case ConsoleKey.F: + textEditor.FormatLines(); + textEditor.Display(true); + WriteStatus("Formatted!"); + return true; } - if (keyInfo.Key == ConsoleKey.DownArrow) + } + if (keyInfo.Key == ConsoleKey.DownArrow) + { + textEditor.CursorPosition.Y++; + if (textEditor.CursorPosition.Y - textEditor.LineOffset >= Console.WindowHeight - 3) { - textEditor.CursorPosition.Y++; - if (textEditor.CursorPosition.Y - textEditor.LineOffset >= Console.WindowHeight - 3) + textEditor.LineOffset++; + textEditor.Display(true); + } + } + else if (keyInfo.Key == ConsoleKey.UpArrow) + { + if (textEditor.CursorPosition.Y > 0) + { + textEditor.CursorPosition.Y--; + if (textEditor.CursorPosition.Y - textEditor.LineOffset < 0 && textEditor.LineOffset > 0) { - textEditor.LineOffset++; + textEditor.LineOffset--; textEditor.Display(true); } } - else if (keyInfo.Key == ConsoleKey.UpArrow) + } + else if (keyInfo.Key == ConsoleKey.LeftArrow) + { + if (textEditor.CursorPosition.X > 0) { - if (textEditor.CursorPosition.Y > 0) - { - textEditor.CursorPosition.Y--; - if (textEditor.CursorPosition.Y - textEditor.LineOffset < 0 && textEditor.LineOffset > 0) - { - textEditor.LineOffset--; - textEditor.Display(true); - } - } - } - else if (keyInfo.Key == ConsoleKey.LeftArrow) - { - if (textEditor.CursorPosition.X > 0) - { - textEditor.CursorPosition.X--; - } - } - else if (keyInfo.Key == ConsoleKey.RightArrow) - { - if (textEditor.CursorPosition.X + textEditor.GetSpacing() + 3 < Console.WindowWidth) - { - textEditor.CursorPosition.X++; - } - } - else if (keyInfo.Key == ConsoleKey.Enter) - { - while (textEditor.Lines.Count <= textEditor.CursorPosition.Y) - { - textEditor.Lines.Add(""); - } - textEditor.Lines.Insert(textEditor.CursorPosition.Y, ""); - - string line = textEditor.Lines[textEditor.CursorPosition.Y + 1]; - - textEditor.Lines[textEditor.CursorPosition.Y] = line[..Math.Min(textEditor.CursorPosition.X, line.Length)]; - textEditor.Lines[textEditor.CursorPosition.Y + 1] = line[Math.Min(textEditor.CursorPosition.X, line.Length)..]; - - textEditor.CursorPosition.X = 0; - textEditor.CursorPosition.Y++; - - if (textEditor.CursorPosition.Y - textEditor.LineOffset >= Console.WindowHeight - 3) - { - textEditor.LineOffset++; - } - textEditor.Display(true); - } - else - { - while (textEditor.Lines.Count <= textEditor.CursorPosition.Y) - { - textEditor.Lines.Add(""); - } - - StringBuilder lineBuilder = new StringBuilder(textEditor.Lines[textEditor.CursorPosition.Y]); - while (lineBuilder.Length <= textEditor.CursorPosition.X) - { - lineBuilder.Append(' '); - } - - textEditor.Lines[textEditor.CursorPosition.Y] = lineBuilder.ToString(); - - if (keyInfo.Key == ConsoleKey.Backspace) - { - if (textEditor.CursorPosition.X > 0) - { - if (textEditor.Lines[textEditor.CursorPosition.Y][textEditor.CursorPosition.X - 1] == ' ' && textEditor.CursorPosition.X > textEditor.Lines[textEditor.CursorPosition.Y].TrimEnd().Length) - { - textEditor.Lines[textEditor.CursorPosition.Y] = textEditor.Lines[textEditor.CursorPosition.Y].TrimEnd(); - textEditor.CursorPosition.X = textEditor.Lines[textEditor.CursorPosition.Y].Length; - } - else - { - textEditor.Lines[textEditor.CursorPosition.Y] = textEditor.Lines[textEditor.CursorPosition.Y].Remove(textEditor.CursorPosition.X - 1, 1); - textEditor.CursorPosition.X--; - } - } - else - { - if (textEditor.CursorPosition.Y > 0) - { - if (string.IsNullOrWhiteSpace(textEditor.Lines[textEditor.CursorPosition.Y - 1])) - { - textEditor.Lines.RemoveAt(--textEditor.CursorPosition.Y); - } - else - { - textEditor.CursorPosition.Y--; - textEditor.CursorPosition.X = textEditor.Lines[textEditor.CursorPosition.Y].TrimEnd().Length; - textEditor.Lines[textEditor.CursorPosition.Y] += textEditor.Lines[textEditor.CursorPosition.Y + 1]; - textEditor.Lines.RemoveAt(textEditor.CursorPosition.Y + 1); - } - if (textEditor.CursorPosition.Y - textEditor.LineOffset < 0 && textEditor.LineOffset > 0) - { - textEditor.LineOffset--; - } - textEditor.Display(true); - } - } - - while (textEditor.Lines.Count > 0 && string.IsNullOrWhiteSpace(textEditor.Lines[^1])) - { - textEditor.Lines.RemoveAt(textEditor.Lines.Count - 1); - } - } - else if (textEditor.CursorPosition.X + textEditor.GetSpacing() + 3 < Console.WindowWidth) - { - char input = keyInfo.KeyChar; - if (!char.IsControl(input)) - { - textEditor.Lines[textEditor.CursorPosition.Y] = textEditor.Lines[textEditor.CursorPosition.Y].Insert(textEditor.CursorPosition.X, input.ToString()); - textEditor.CursorPosition.X++; - } - } + textEditor.CursorPosition.X--; } } - else if (textEditor.EditMode == Mode.Command) + else if (keyInfo.Key == ConsoleKey.RightArrow) { - Console.SetCursorPosition(3, Console.WindowHeight - 2); - - string input = Console.ReadLine() ?? string.Empty; - string command = input.Split(' ')[0].Trim(); - string path; - - Console.CursorVisible = false; - - switch (command) + if (textEditor.CursorPosition.X + textEditor.GetSpacing() + 3 < Console.WindowWidth) { - case "edit": - WriteStatus(string.Empty); - textEditor.EditMode = Mode.Edit; - break; + textEditor.CursorPosition.X++; + } + } + else if (keyInfo.Key == ConsoleKey.Enter) + { + while (textEditor.Lines.Count <= textEditor.CursorPosition.Y) + { + textEditor.Lines.Add(""); + } + textEditor.Lines.Insert(textEditor.CursorPosition.Y, ""); - case "line": - WriteStatus(string.Empty); + string line = textEditor.Lines[textEditor.CursorPosition.Y + 1]; - bool success = false; - int lineNumber = 0; - if (input.Split(' ').Length == 2) - { - success = int.TryParse(input.Split(' ')[1], out lineNumber); - } + textEditor.Lines[textEditor.CursorPosition.Y] = line[..Math.Min(textEditor.CursorPosition.X, line.Length)]; + textEditor.Lines[textEditor.CursorPosition.Y + 1] = line[Math.Min(textEditor.CursorPosition.X, line.Length)..]; - if (success && lineNumber > 0) - { - textEditor.CursorPosition.Y = lineNumber - 1; - textEditor.CursorPosition.X = 0; - textEditor.LineOffset = lineNumber - 1; - textEditor.EditMode = Mode.Edit; - } - else - { - WriteStatus("Invalid line number!"); - } - break; + textEditor.CursorPosition.X = 0; + textEditor.CursorPosition.Y++; - case "save": - textEditor.Save(input, false); - break; - - case "run": - if (textEditor.Save(input, true)) - { - textEditor.EditMode = Mode.Debug; - Console.Clear(); - Console.CursorVisible = true; - textEditor.YesNtInterpreter.Execute(textEditor.CurrentPath); - while (Console.KeyAvailable) - { - Console.ReadKey(true); - } - Console.ReadKey(); - WriteStatus(string.Empty); - textEditor.EditMode = Mode.Command; - } - break; - - case "debug": - if (textEditor.Save(input, true)) - { - textEditor.EditMode = Mode.Debug; - Console.Clear(); - Console.CursorVisible = true; - textEditor.YesNtInterpreter.Execute(textEditor.CurrentPath, true); - while (Console.KeyAvailable) - { - Console.ReadKey(true); - } - Console.ReadKey(); - WriteStatus(string.Empty); - textEditor.EditMode = Mode.Command; - } - break; - - case "load": - if (input.Split(' ').Length == 2) - { - path = input.Split(' ')[1]; - } - else - { - WriteStatus("Invalid arguments!"); - break; - } - - try - { - textEditor.Load(path); - } - catch (Exception ex) - { - WriteStatus(ex.Message); - } - textEditor.LineOffset = 0; - textEditor.CursorPosition.X = 0; - textEditor.CursorPosition.Y = 0; - break; - - case "new": - - textEditor.LineOffset = 0; - textEditor.CursorPosition.X = 0; - textEditor.CursorPosition.Y = 0; - textEditor.CurrentPath = string.Empty; - textEditor.Lines.Clear(); - WriteStatus(string.Empty); - break; - - case "exit": - return false; - - default: - WriteStatus("Command not found!"); - break; + if (textEditor.CursorPosition.Y - textEditor.LineOffset >= Console.WindowHeight - 3) + { + textEditor.LineOffset++; } textEditor.Display(true); } - return true; + else + { + while (textEditor.Lines.Count <= textEditor.CursorPosition.Y) + { + textEditor.Lines.Add(""); + } + + StringBuilder lineBuilder = new StringBuilder(textEditor.Lines[textEditor.CursorPosition.Y]); + while (lineBuilder.Length <= textEditor.CursorPosition.X) + { + _ = lineBuilder.Append(' '); + } + + textEditor.Lines[textEditor.CursorPosition.Y] = lineBuilder.ToString(); + + if (keyInfo.Key is ConsoleKey.Backspace or ConsoleKey.Delete) + { + if (keyInfo.Key == ConsoleKey.Delete) + { + textEditor.CursorPosition.X++; + } + + if (textEditor.CursorPosition.X > 0) + { + if (textEditor.Lines[textEditor.CursorPosition.Y][textEditor.CursorPosition.X - 1] == ' ' && textEditor.CursorPosition.X > textEditor.Lines[textEditor.CursorPosition.Y].TrimEnd().Length) + { + textEditor.Lines[textEditor.CursorPosition.Y] = textEditor.Lines[textEditor.CursorPosition.Y].TrimEnd(); + textEditor.CursorPosition.X = textEditor.Lines[textEditor.CursorPosition.Y].Length; + } + else + { + textEditor.Lines[textEditor.CursorPosition.Y] = textEditor.Lines[textEditor.CursorPosition.Y].Remove(textEditor.CursorPosition.X - 1, 1); + textEditor.CursorPosition.X--; + } + } + else + { + if (textEditor.CursorPosition.Y > 0) + { + if (string.IsNullOrWhiteSpace(textEditor.Lines[textEditor.CursorPosition.Y - 1])) + { + textEditor.Lines.RemoveAt(--textEditor.CursorPosition.Y); + } + else + { + textEditor.CursorPosition.Y--; + textEditor.CursorPosition.X = textEditor.Lines[textEditor.CursorPosition.Y].TrimEnd().Length; + textEditor.Lines[textEditor.CursorPosition.Y] += textEditor.Lines[textEditor.CursorPosition.Y + 1]; + textEditor.Lines.RemoveAt(textEditor.CursorPosition.Y + 1); + } + if (textEditor.CursorPosition.Y - textEditor.LineOffset < 0 && textEditor.LineOffset > 0) + { + textEditor.LineOffset--; + } + textEditor.Display(true); + } + } + + while (textEditor.Lines.Count > 0 && string.IsNullOrWhiteSpace(textEditor.Lines[^1])) + { + textEditor.Lines.RemoveAt(textEditor.Lines.Count - 1); + } + } + else if (textEditor.CursorPosition.X + textEditor.GetSpacing() + 3 < Console.WindowWidth) + { + char input = keyInfo.KeyChar; + if (!char.IsControl(input)) + { + textEditor.Lines[textEditor.CursorPosition.Y] = textEditor.Lines[textEditor.CursorPosition.Y].Insert(textEditor.CursorPosition.X, input.ToString()); + textEditor.CursorPosition.X++; + } + } + } + } + else if (textEditor.EditMode == Mode.Command) + { + Console.SetCursorPosition(3, Console.WindowHeight - 2); + + string input = Console.ReadLine() ?? string.Empty; + string command = input.Split(' ')[0].Trim(); + string path; + + Console.CursorVisible = false; + + switch (command) + { + case "edit": + WriteStatus(string.Empty); + textEditor.EditMode = Mode.Edit; + break; + + case "line": + WriteStatus(string.Empty); + + bool success = false; + int lineNumber = 0; + if (input.Split(' ').Length == 2) + { + success = int.TryParse(input.Split(' ')[1], out lineNumber); + } + + if (success && lineNumber > 0) + { + textEditor.CursorPosition.Y = lineNumber - 1; + textEditor.CursorPosition.X = 0; + textEditor.LineOffset = lineNumber - 1; + textEditor.EditMode = Mode.Edit; + } + else + { + WriteStatus("Invalid line number!"); + } + break; + + case "save": + _ = textEditor.Save(input, false); + break; + + case "run": + ExecuteWithDebugScreen(input, false, false); + break; + + case "debug": + if (TryParseDebugCommand(input, out bool stepMode, out string parsedPath)) + { + string saveInput = string.IsNullOrWhiteSpace(parsedPath) ? "debug" : $"debug {parsedPath}"; + ExecuteWithDebugScreen(saveInput, true, stepMode); + } + else + { + WriteStatus("Invalid arguments!"); + } + break; + + case "load": + if (input.Split(' ').Length == 2) + { + path = input.Split(' ')[1]; + } + else + { + WriteStatus("Invalid arguments!"); + break; + } + + try + { + _ = textEditor.Load(path); + } + catch (Exception ex) + { + WriteStatus(ex.Message); + } + textEditor.LineOffset = 0; + textEditor.CursorPosition.X = 0; + textEditor.CursorPosition.Y = 0; + break; + + case "new": + + textEditor.LineOffset = 0; + textEditor.CursorPosition.X = 0; + textEditor.CursorPosition.Y = 0; + textEditor.CurrentPath = string.Empty; + textEditor.Lines.Clear(); + WriteStatus(string.Empty); + break; + + case "format": + textEditor.FormatLines(); + WriteStatus("Formatted!"); + break; + + case "exit": + return false; + + default: + WriteStatus("Command not found!"); + break; + } + textEditor.Display(true); + } + return true; + } + + internal static void WriteStatus(string input) + { + Console.SetCursorPosition(0, Console.WindowHeight - 1); + Console.Write(input + new string(' ', Console.WindowWidth - input.Length - 1)); + } + + private static bool TryParseDebugCommand(string input, out bool stepMode, out string path) + { + stepMode = false; + path = string.Empty; + + string[] parts = input.Split(' ', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0 || !parts[0].Equals("debug", StringComparison.OrdinalIgnoreCase)) + { + return false; } - internal static void WriteStatus(string input) + for (int i = 1; i < parts.Length; i++) { - Console.SetCursorPosition(0, Console.WindowHeight - 1); - Console.Write(input + new string(' ', Console.WindowWidth - input.Length - 1)); + string token = parts[i]; + if (token.Equals("step", StringComparison.OrdinalIgnoreCase)) + { + if (stepMode) + { + return false; + } + + stepMode = true; + continue; + } + + if (string.IsNullOrWhiteSpace(path)) + { + path = token; + continue; + } + + return false; } + + return true; + } + + private void ExecuteWithDebugScreen(string saveInput, bool debugMode, bool stepMode) + { + string[] parts = saveInput.Split(' ', StringSplitOptions.RemoveEmptyEntries); + bool hasPathArgument = parts.Length > 1; + bool canRunUnsavedBuffer = !hasPathArgument && string.IsNullOrWhiteSpace(textEditor.CurrentPath); + + bool canExecute = canRunUnsavedBuffer || textEditor.Save(saveInput, true); + if (!canExecute) + { + return; + } + + Mode previousMode = textEditor.EditMode; + + textEditor.EditMode = Mode.Debug; + textEditor.IsStepDebugMode = stepMode; + Console.Clear(); + Console.CursorVisible = true; + + if (canRunUnsavedBuffer) + { + textEditor.YesNtInterpreter.Execute([.. textEditor.Lines], debugMode); + } + else + { + textEditor.YesNtInterpreter.Execute(textEditor.CurrentPath, debugMode); + } + + while (Console.KeyAvailable) + { + _ = Console.ReadKey(true); + } + _ = Console.ReadKey(); + WriteStatus(string.Empty); + textEditor.IsStepDebugMode = false; + textEditor.EditMode = previousMode; } } \ No newline at end of file diff --git a/YesNt.CodeEditor/Program.cs b/YesNt.CodeEditor/Program.cs index d95b4a7..5b5517b 100644 --- a/YesNt.CodeEditor/Program.cs +++ b/YesNt.CodeEditor/Program.cs @@ -1,21 +1,4 @@ -namespace YesNt.CodeEditor -{ - internal static class Program - { - private static void Main(string[] args) - { - TextEditor textEditor; +using YesNt.CodeEditor; - if (args.Length > 0) - { - textEditor = new TextEditor(args[0]); - } - else - { - textEditor = new TextEditor(); - } - - textEditor.Run(); - } - } -} \ No newline at end of file +TextEditor textEditor = args.Length > 0 ? new TextEditor(args[0]) : new TextEditor(); +textEditor.Run(); \ No newline at end of file diff --git a/YesNt.CodeEditor/Properties/launchSettings.json b/YesNt.CodeEditor/Properties/launchSettings.json new file mode 100644 index 0000000..d1da8c5 --- /dev/null +++ b/YesNt.CodeEditor/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "YesNt.CodeEditor": { + "commandName": "Project" + }, + "WSL": { + "commandName": "WSL2", + "environmentVariables": {}, + "distributionName": "" + } + } +} \ No newline at end of file diff --git a/YesNt.CodeEditor/SyntaxHighlighter.cs b/YesNt.CodeEditor/SyntaxHighlighter.cs index d85fc6b..061d834 100644 --- a/YesNt.CodeEditor/SyntaxHighlighter.cs +++ b/YesNt.CodeEditor/SyntaxHighlighter.cs @@ -1,159 +1,176 @@ using System; using System.Collections.ObjectModel; +using System.Linq; using System.Text.RegularExpressions; using YesNt.Interpreter.Enums; using YesNt.Interpreter.Runtime; using YesNt.Interpreter.Utilities; -namespace YesNt.CodeEditor +namespace YesNt.CodeEditor; + +internal partial class SyntaxHighlighter(ReadOnlyCollection statementInformation) { - internal class SyntaxHighlighter + private readonly ReadOnlyCollection statementInformation = statementInformation; + private readonly string[] replacementValues = StringExtensions.ReplacementRules.Values.ToArray(); + + public static string Base64Encode(string plainText) { - private readonly ReadOnlyCollection statementInformation; + byte[] plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText); + return System.Convert.ToBase64String(plainTextBytes); + } - public SyntaxHighlighter(ReadOnlyCollection statementInformation) + public static string Base64Decode(string base64EncodedData) + { + byte[] base64EncodedBytes = System.Convert.FromBase64String(base64EncodedData); + return System.Text.Encoding.UTF8.GetString(base64EncodedBytes); + } + + public void Write(string input) + { + input = input.Replace("\0", string.Empty); + + if (input.TrimStart(' ').StartsWith('#')) { - this.statementInformation = statementInformation; + input = AddColorInformation(input, input, ConsoleColor.Gray, SearchMode.Exact); } - - public void Write(string input) + else { - input = input.Replace("\0", string.Empty); - - if (input.StartsWith('#')) + MatchCollection matches = StringRegex().Matches(input); + for (int i = 0; i < matches.Count; i++) { - input = AddColorInformation(input, input, ConsoleColor.Gray, SearchMode.Exact); + input = AddColorInformation(input, matches[i].Value, ConsoleColor.DarkYellow, SearchMode.Contains); } - else + + matches = VariableRegex().Matches(input); + for (int i = 0; i < matches.Count; i++) { - MatchCollection matches = Regex.Matches(input, @"!!."); - for (int i = 0; i < matches.Count; i++) + input = AddColorInformation(input, matches[i].Value, ConsoleColor.Cyan, SearchMode.Contains); + } + + for (int i = 0; i < replacementValues.Length; i++) + { + input = AddColorInformation(input, replacementValues[i], ConsoleColor.Blue, SearchMode.Contains); + } + + foreach (StatementInformation statement in statementInformation) + { + if (statement.IgnoreSyntaxHighlighting) { - input = AddColorInformation(input, matches[i].Value, Console.ForegroundColor, SearchMode.Contains); + continue; } - foreach (StatementInformation statement in statementInformation) + string name = statement.SpaceAround switch { - if (statement.IgnoreSyntaxHighlighting) + SpaceAround.StartEnd => $" {statement.Name.Trim()} ", + SpaceAround.Start => $" {statement.Name.Trim()}", + SpaceAround.End => $"{statement.Name.Trim()} ", + _ => statement.Name.Trim() + }; + input = input.TrimEnd(' '); + string inputTrim = input.Trim(' '); + if (statement.SearchMode == SearchMode.StartOfLine && inputTrim.StartsWith(name)) + { + if (statement.Separator is not null && input.Contains(statement.Separator)) + { + input = AddColorInformation(input, statement.Separator, statement.Color, SearchMode.StartOfLine); + } + else if (statement.Separator is not null) { continue; } - - string name = statement.SpaceAround switch - { - SpaceAround.StartEnd => $" {statement.Name.Trim()} ", - SpaceAround.Start => $" {statement.Name.Trim()}", - SpaceAround.End => $"{statement.Name.Trim()} ", - _ => statement.Name - }; - input = input.TrimEnd(); - if (statement.SearchMode == SearchMode.StartOfLine && input.StartsWith(name)) - { - if (statement.Seperator is not null && input.Contains(statement.Seperator)) - { - input = AddColorInformation(input, statement.Seperator, statement.Color, SearchMode.StartOfLine); - } - else if (statement.Seperator is not null) - { - continue; - } - input = AddColorInformation(input, input[..name.Length], statement.Color, statement.SearchMode); - } - if (statement.SearchMode == SearchMode.Contains && $" {input} ".Contains(name)) - { - if (statement.Seperator is not null && input.Contains(statement.Seperator)) - { - input = AddColorInformation(input, statement.Seperator, statement.Color, SearchMode.StartOfLine); - } - else if (statement.Seperator is not null) - { - continue; - } - input = AddColorInformation($"{input} ", $"{input} ".Substring($"{input} ".IndexOf(name), name.Length), statement.Color, statement.SearchMode); - } - if (statement.SearchMode == SearchMode.EndOfLine && input.EndsWith(name)) - { - if (statement.Seperator is not null && input.Contains(statement.Seperator)) - { - input = AddColorInformation(input, statement.Seperator, statement.Color, SearchMode.StartOfLine); - } - else if (statement.Seperator is not null) - { - continue; - } - input = AddColorInformation(input, input[^name.Length..], statement.Color, statement.SearchMode); - } - if (statement.SearchMode == SearchMode.Exact && input.Equals(name)) - { - if (statement.Seperator is not null && input.Contains(statement.Seperator)) - { - input = AddColorInformation(input, statement.Seperator, statement.Color, SearchMode.StartOfLine); - } - else if (statement.Seperator is not null) - { - continue; - } - input = AddColorInformation(input, input, statement.Color, statement.SearchMode); - } + input = AddColorInformation(input, name, statement.Color, statement.SearchMode); } - - matches = Regex.Matches(input, @">[a-zA-Z0-9]+"); - for (int i = 0; i < matches.Count; i++) + else if (statement.SearchMode == SearchMode.Contains && input.Contains(name)) { - input = AddColorInformation(input, matches[i].Value, ConsoleColor.Cyan, SearchMode.Contains); + if (statement.Separator is not null && input.Contains(statement.Separator)) + { + input = AddColorInformation(input, statement.Separator, statement.Color, SearchMode.StartOfLine); + } + else if (statement.Separator is not null) + { + continue; + } + input = AddColorInformation(input, input.Substring(input.IndexOf(name), name.Length), statement.Color, statement.SearchMode); } - - matches = Regex.Matches(input, @"^<[a-zA-Z0-9]+"); - for (int i = 0; i < matches.Count; i++) + else if (statement.SearchMode == SearchMode.EndOfLine && input.EndsWith(name)) { - input = AddColorInformation(input, matches[i].Value, ConsoleColor.DarkCyan, SearchMode.StartOfLine); + if (statement.Separator is not null && input.Contains(statement.Separator)) + { + input = AddColorInformation(input, statement.Separator, statement.Color, SearchMode.StartOfLine); + } + else if (statement.Separator is not null) + { + continue; + } + input = AddColorInformation(input, input[^name.Length..], statement.Color, statement.SearchMode); } - - matches = Regex.Matches(input, @"^!<[a-zA-Z0-9]+"); - for (int i = 0; i < matches.Count; i++) + else if (statement.SearchMode == SearchMode.Exact && inputTrim.Equals(name)) { - input = AddColorInformation(input, matches[i].Value, ConsoleColor.Blue, SearchMode.StartOfLine); + if (statement.Separator is not null && input.Contains(statement.Separator)) + { + input = AddColorInformation(input, statement.Separator, statement.Color, SearchMode.StartOfLine); + } + else if (statement.Separator is not null) + { + continue; + } + input = AddColorInformation(input, input, statement.Color, statement.SearchMode); } } - - foreach (string part in input.Split("\0")) - { - ConsoleColor consoleColor; - string messagePart = part; - - string stringColor = Regex.Match(messagePart, "(?<=(\\r))(.*)(?=\\r)").Value; - bool succ = int.TryParse(stringColor, out int colorIndex); - if (succ && colorIndex >= 0 && colorIndex < 16) - { - messagePart = messagePart.Replace($"\r{stringColor}\r", string.Empty); - consoleColor = (ConsoleColor)colorIndex; - } - else - { - consoleColor = ConsoleColor.White; - } - - if (consoleColor == Console.BackgroundColor) - { - consoleColor = ConsoleColor.White; - } - Console.ForegroundColor = consoleColor; - Console.Write(messagePart); - } - Console.ForegroundColor = ConsoleColor.Gray; } - private static string AddColorInformation(string originalString, string value, ConsoleColor color, SearchMode searchMode) + foreach (string part in input.Split("\0")) { - int spacesAtEnd = value.WhiteSpaceAtEnd(); - string reult = searchMode switch + ConsoleColor consoleColor; + string messagePart = part; + + string stringColor = StringColorRegex().Match(messagePart).Value; + bool succ = int.TryParse(stringColor, out int colorIndex); + if (succ && colorIndex >= 0 && colorIndex < 16) { - SearchMode.StartOfLine => originalString.ReplaceFirstOccurrence(value, $"\0\r{(int)color}\r{value.TrimEnd()}\0" + new string(' ', spacesAtEnd)), - SearchMode.EndOfLine => originalString.ReplaceLastOccurrence(value, $"\0\r{(int)color}\r{value.TrimEnd()}\0" + new string(' ', spacesAtEnd)), - _ => originalString.Replace(value, $"\0\r{(int)color}\r{value.TrimEnd()}\0" + new string(' ', spacesAtEnd)) - }; - return reult; + messagePart = Base64Decode(messagePart.Replace("\x01" + stringColor + "\x01", string.Empty)); + consoleColor = (ConsoleColor)colorIndex; + } + else + { + consoleColor = ConsoleColor.White; + } + + if (consoleColor == Console.BackgroundColor) + { + consoleColor = ConsoleColor.White; + } + Console.ForegroundColor = consoleColor; + Console.Write(messagePart); } + Console.ForegroundColor = ConsoleColor.Gray; } + + private static string AddColorInformation(string originalString, string value, ConsoleColor color, SearchMode searchMode) + { + int spacesAtEnd = value.WhiteSpaceAtEnd(); + + string base64Value = Base64Encode(value.TrimEnd()); + + string result = searchMode switch + { + SearchMode.StartOfLine => originalString.ReplaceFirstOccurrence(value, $"\0\x01{(int)color}\x01{base64Value}\0" + new string(' ', spacesAtEnd)), + SearchMode.EndOfLine => originalString.ReplaceLastOccurrence(value, $"\0\x01{(int)color}\x01{base64Value}\0" + new string(' ', spacesAtEnd)), + _ => originalString.Replace(value, $"\0\x01{(int)color}\x01{base64Value}\0" + new string(' ', spacesAtEnd)) + }; + return result; + } + + // This regex matches variables in the format ${variableName}, where variableName consists of alphanumeric characters. + [GeneratedRegex("\\$\\{[a-zA-Z0-9]+\\}")] + private static partial Regex VariableRegex(); + + // This regex matches string literals, taking into account escaped quotes and backslashes. + [GeneratedRegex(@"(? Exe - net5.0 + net10.0 + AnyCPU;x64 + True + yesntcode + + + + diff --git a/YesNt.Interpreter.App/Program.cs b/YesNt.Interpreter.App/Program.cs new file mode 100644 index 0000000..553cf8a --- /dev/null +++ b/YesNt.Interpreter.App/Program.cs @@ -0,0 +1,20 @@ +using System; +using System.IO; + +using YesNt.Interpreter.Runtime; + +if (args.Length == 1) +{ + if (!File.Exists(args[0])) + { + Console.WriteLine("File not found: " + args[0]); + return; + } + + YesNtInterpreter interpreter = new YesNtInterpreter(); + interpreter.Execute(args[0]); +} +else +{ + Console.WriteLine("No path specified!"); +} \ No newline at end of file diff --git a/YesNt.Interpreter.App/YesNt.Interpreter.App.csproj b/YesNt.Interpreter.App/YesNt.Interpreter.App.csproj new file mode 100644 index 0000000..22c4adc --- /dev/null +++ b/YesNt.Interpreter.App/YesNt.Interpreter.App.csproj @@ -0,0 +1,15 @@ + + + + net10.0 + Exe + AnyCPU;x64 + True + yesnt + + + + + + + diff --git a/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs b/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs new file mode 100644 index 0000000..242902d --- /dev/null +++ b/YesNt.Interpreter.Generator/StatementRegistryGenerator.cs @@ -0,0 +1,237 @@ +using Microsoft.CodeAnalysis; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace YesNt.Interpreter.Generator; + +[Generator] +public sealed class StatementRegistryGenerator : IIncrementalGenerator +{ + private const string StatementAttributeName = "YesNt.Interpreter.Attributes.StatementAttribute"; + private const string StaticStatementAttributeName = "YesNt.Interpreter.Attributes.StaticStatementAttribute"; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterSourceOutput( + context.CompilationProvider, + Execute); + } + + private static void Execute(SourceProductionContext context, Compilation compilation) + { + List statementMethods = []; + List staticStatementMethods = []; + + CollectMethods(compilation.Assembly.GlobalNamespace, statementMethods, staticStatementMethods); + + string source = GenerateRegistrySource(statementMethods, staticStatementMethods); + context.AddSource("GeneratedStatementRegistry.g.cs", source); + } + + private static void CollectMethods( + INamespaceSymbol namespaceSymbol, + List statementMethods, + List staticStatementMethods) + { + foreach (INamespaceSymbol childNamespace in namespaceSymbol.GetNamespaceMembers()) + { + CollectMethods(childNamespace, statementMethods, staticStatementMethods); + } + + foreach (INamedTypeSymbol type in namespaceSymbol.GetTypeMembers()) + { + CollectMethods(type, statementMethods, staticStatementMethods); + } + } + + private static void CollectMethods( + INamedTypeSymbol typeSymbol, + List statementMethods, + List staticStatementMethods) + { + foreach (ISymbol member in typeSymbol.GetMembers()) + { + if (member is IMethodSymbol method && method.MethodKind == MethodKind.Ordinary) + { + foreach (AttributeData attribute in method.GetAttributes()) + { + string? attributeName = attribute.AttributeClass?.ToDisplayString(); + if (attributeName == StatementAttributeName) + { + statementMethods.Add(new MethodRegistration(typeSymbol, method, attribute)); + } + else if (attributeName == StaticStatementAttributeName) + { + staticStatementMethods.Add(new MethodRegistration(typeSymbol, method, attribute)); + } + } + } + } + + foreach (INamedTypeSymbol nestedType in typeSymbol.GetTypeMembers()) + { + CollectMethods(nestedType, statementMethods, staticStatementMethods); + } + } + + private static string GenerateRegistrySource( + List statementMethods, + List staticStatementMethods) + { + StringBuilder sb = new StringBuilder(); + + _ = sb.AppendLine("// "); + _ = sb.AppendLine("#nullable enable"); + _ = sb.AppendLine("using System;"); + _ = sb.AppendLine("using System.Collections.Generic;"); + _ = sb.AppendLine("using System.Linq;"); + _ = sb.AppendLine(); + _ = sb.AppendLine("namespace YesNt.Interpreter.Runtime;"); + _ = sb.AppendLine(); + _ = sb.AppendLine("internal static class GeneratedStatementRegistry"); + _ = sb.AppendLine("{"); + _ = sb.AppendLine(" internal static void Register("); + _ = sb.AppendLine(" RuntimeInformation runtimeInfo,"); + _ = sb.AppendLine(" out Dictionary> statements,"); + _ = sb.AppendLine(" out List> staticStatements)"); + _ = sb.AppendLine(" {"); + + List allTypes = statementMethods + .Concat(staticStatementMethods) + .Select(x => x.ContainingType) + .GroupBy(x => x, SymbolEqualityComparer.Default) + .Select(g => g.First()) + .OrderBy(x => x.ToDisplayString()) + .ToList(); + + Dictionary instanceNames = new Dictionary(SymbolEqualityComparer.Default); + int index = 0; + foreach (INamedTypeSymbol type in allTypes) + { + string instanceName = $"instance{index++}"; + instanceNames[type] = instanceName; + _ = sb.AppendLine($" var {instanceName} = new global::{type.ToDisplayString()}();"); + _ = sb.AppendLine($" {instanceName}.RuntimeInfo = runtimeInfo;"); + } + + _ = sb.AppendLine(" var statementEntries = new List>>();"); + + foreach (MethodRegistration method in statementMethods + .OrderBy(x => x.ContainingType.ToDisplayString()) + .ThenBy(x => x.Method.Name)) + { + string instanceName = instanceNames[method.ContainingType]; + string attributeCreation = BuildAttributeCreation("global::YesNt.Interpreter.Attributes.StatementAttribute", method.Attribute); + _ = sb.AppendLine($" statementEntries.Add(new({attributeCreation}, {instanceName}.{method.Method.Name}));"); + } + + _ = sb.AppendLine(" var staticEntries = new List>();"); + + foreach (MethodRegistration method in staticStatementMethods + .OrderBy(x => x.ContainingType.ToDisplayString()) + .ThenBy(x => x.Method.Name)) + { + string instanceName = instanceNames[method.ContainingType]; + string attributeCreation = BuildAttributeCreation("global::YesNt.Interpreter.Attributes.StaticStatementAttribute", method.Attribute); + _ = sb.AppendLine($" staticEntries.Add(new({attributeCreation}, {instanceName}.{method.Method.Name}));"); + } + + _ = sb.AppendLine(" statements = statementEntries"); + _ = sb.AppendLine(" .OrderBy(s => s.Key.Priority)"); + _ = sb.AppendLine(" .ThenByDescending(s => s.Key.Name.Length)"); + _ = sb.AppendLine(" .ToDictionary(x => x.Key, x => x.Value);"); + _ = sb.AppendLine(); + _ = sb.AppendLine(" staticStatements = staticEntries"); + _ = sb.AppendLine(" .OrderBy(s => s.Key.Priority)"); + _ = sb.AppendLine(" .ToList();"); + _ = sb.AppendLine(" }"); + _ = sb.AppendLine("}"); + + return sb.ToString(); + } + + private static string BuildAttributeCreation(string attributeTypeName, AttributeData attribute) + { + string ctorArgs = string.Join(", ", + attribute.ConstructorArguments.Select(ToLiteral)); + + string creation = $"new {attributeTypeName}({ctorArgs})"; + + if (attribute.NamedArguments.Length == 0) + { + return creation; + } + + string namedArgs = string.Join(", ", + attribute.NamedArguments.Select(arg => $"{arg.Key} = {ToLiteral(arg.Value)}")); + + return $"{creation} {{ {namedArgs} }}"; + } + + private static string ToLiteral(TypedConstant constant) + { + if (constant.IsNull) + { + return "null!"; + } + + if (constant.Type is null) + { + return "null!"; + } + + if (constant.Kind == TypedConstantKind.Enum) + { + string enumType = $"global::{constant.Type.ToDisplayString()}"; + object value = constant.Value!; + return $"({enumType}){Convert.ToInt64(value)}"; + } + + return constant.Type.SpecialType switch + { + SpecialType.System_String => "\"" + EscapeString((string)constant.Value!) + "\"", + SpecialType.System_Char => "'" + EscapeChar((char)constant.Value!) + "'", + SpecialType.System_Boolean => (bool)constant.Value! ? "true" : "false", + SpecialType.System_Int32 => ((int)constant.Value!).ToString(System.Globalization.CultureInfo.InvariantCulture), + SpecialType.System_Int64 => ((long)constant.Value!).ToString(System.Globalization.CultureInfo.InvariantCulture) + "L", + SpecialType.System_Single => ((float)constant.Value!).ToString(System.Globalization.CultureInfo.InvariantCulture) + "f", + SpecialType.System_Double => ((double)constant.Value!).ToString(System.Globalization.CultureInfo.InvariantCulture), + _ => constant.Value!.ToString() ?? "null!" + }; + } + + private static string EscapeString(string value) + { + return value + .Replace("\\", "\\\\") + .Replace("\"", "\\\"") + .Replace("\r", "\\r") + .Replace("\n", "\\n") + .Replace("\t", "\\t"); + } + + private static string EscapeChar(char value) + { + return value switch + { + '\\' => "\\\\", + '\'' => "\\'", + '\r' => "\\r", + '\n' => "\\n", + '\t' => "\\t", + _ => value.ToString() + }; + } + + private sealed class MethodRegistration(INamedTypeSymbol containingType, IMethodSymbol method, AttributeData attribute) + { + public INamedTypeSymbol ContainingType { get; } = containingType; + + public IMethodSymbol Method { get; } = method; + + public AttributeData Attribute { get; } = attribute; + } +} diff --git a/YesNt.Interpreter.Generator/YesNt.Interpreter.Generator.csproj b/YesNt.Interpreter.Generator/YesNt.Interpreter.Generator.csproj new file mode 100644 index 0000000..82dcc9b --- /dev/null +++ b/YesNt.Interpreter.Generator/YesNt.Interpreter.Generator.csproj @@ -0,0 +1,13 @@ + + + netstandard2.0 + latest + enable + true + true + + + + + + diff --git a/YesNt.Interpreter.Tests/AddStatementTests.cs b/YesNt.Interpreter.Tests/AddStatementTests.cs new file mode 100644 index 0000000..f604839 --- /dev/null +++ b/YesNt.Interpreter.Tests/AddStatementTests.cs @@ -0,0 +1,377 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System.Collections.Generic; + +using YesNt.Interpreter.Attributes; +using YesNt.Interpreter.Enums; +using YesNt.Interpreter.Runtime; + +namespace YesNt.Interpreter.Tests; + +[TestClass] +public class AddStatementTests +{ + [TestMethod] + public void AddStatementStartOfLineExecutesHandlerTest() + { + List lines = + [ + "my_command hello" + ]; + + string? capturedArgs = null; + + _ = YesNtAssert.GetLastLineWithSetup(lines, interpreter => + { + interpreter.AddStatement("my_command", SearchMode.StartOfLine, SpaceAround.End, args => + { + capturedArgs = args; + }); + }); + + Assert.AreEqual("hello", capturedArgs); + } + + [TestMethod] + public void AddStatementConvenienceOverloadHandlerIsCalledTest() + { + List lines = + [ + "custom_cmd world" + ]; + + bool handlerCalled = false; + + _ = YesNtAssert.GetLastLineWithSetup(lines, interpreter => + { + interpreter.AddStatement("custom_cmd", SearchMode.StartOfLine, SpaceAround.End, _ => + { + handlerCalled = true; + }); + }); + + Assert.IsTrue(handlerCalled); + } + + [TestMethod] + public void AddStatementAttributeOverloadHandlerIsCalledTest() + { + List lines = + [ + "attr_cmd test" + ]; + + bool handlerCalled = false; + + _ = YesNtAssert.GetLastLineWithSetup(lines, interpreter => + { + StatementAttribute attr = new StatementAttribute("attr_cmd", SearchMode.StartOfLine, SpaceAround.End); + interpreter.AddStatement(attr, _ => + { + handlerCalled = true; + }); + }); + + Assert.IsTrue(handlerCalled); + } + + [TestMethod] + public void AddStatementExactSearchModeTest() + { + List lines = + [ + "exact_cmd" + ]; + + bool handlerCalled = false; + + _ = YesNtAssert.GetLastLineWithSetup(lines, interpreter => + { + interpreter.AddStatement("exact_cmd", SearchMode.Exact, SpaceAround.None, _ => + { + handlerCalled = true; + }); + }); + + Assert.IsTrue(handlerCalled); + } + + [TestMethod] + public void AddStatementContainsSearchModeTest() + { + List lines = + [ + "prefix ~mark~ suffix" + ]; + + bool handlerCalled = false; + + _ = YesNtAssert.GetLastLineWithSetup(lines, interpreter => + { + interpreter.AddStatement(" ~mark~ ", SearchMode.Contains, SpaceAround.None, _ => + { + handlerCalled = true; + }); + }); + + Assert.IsTrue(handlerCalled); + } + + [TestMethod] + public void AddStatementEndOfLineSearchModeTest() + { + List lines = + [ + "some text !end" + ]; + + bool handlerCalled = false; + + _ = YesNtAssert.GetLastLineWithSetup(lines, interpreter => + { + interpreter.AddStatement(" !end", SearchMode.EndOfLine, SpaceAround.None, _ => + { + handlerCalled = true; + }); + }); + + Assert.IsTrue(handlerCalled); + } + + [TestMethod] + public void AddStatementReceivesCorrectArgsTest() + { + List lines = + [ + "capture_cmd the quick brown fox" + ]; + + string? capturedArgs = null; + + _ = YesNtAssert.GetLastLineWithSetup(lines, interpreter => + { + interpreter.AddStatement("capture_cmd", SearchMode.StartOfLine, SpaceAround.End, args => + { + capturedArgs = args; + }); + }); + + Assert.AreEqual("the quick brown fox", capturedArgs); + } + + [TestMethod] + public void AddStatementWorksAlongsideBuiltinStatementsTest() + { + List lines = + [ + "custom_log first", + "print_line second" + ]; + + bool customCalled = false; + + YesNtAssert.ContainsDebugOutputWithSetup(lines, "second", interpreter => + { + interpreter.AddStatement("custom_log", SearchMode.StartOfLine, SpaceAround.End, _ => + { + customCalled = true; + }); + }); + + Assert.IsTrue(customCalled); + } + + [TestMethod] + public void AddStatementUnknownStatementFailsTest() + { + List lines = + [ + "unknown_command foo" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Invalid statement"); + } + + [TestMethod] + public void RemoveStatementCausesInvalidStatementTest() + { + List lines = + [ + "sleep 100" + ]; + + YesNtAssert.ContainsTerminationMessageWithSetup(lines, "Invalid statement", interpreter => + { + interpreter.RemoveStatement("sleep"); + }); + } + + [TestMethod] + public void DisabledStatementIsIgnoredTest() + { + List lines = + [ + "var x = hello", + "${x}" + ]; + + YesNtAssert.ContainsTerminationMessageWithSetup(lines, "Variable \"x\" not found", interpreter => + { + interpreter.DisableStatement("var"); + }); + } + + [TestMethod] + public void DisabledStatementCanBeReenabledTest() + { + List lines = + [ + "var result = overwritten", + "${result}" + ]; + + YesNtAssert.IsLastLineEqualWithSetup(lines, "overwritten", setup: interpreter => + { + interpreter.DisableStatement("var"); + interpreter.EnableStatement("var"); + }); + } + + [TestMethod] + public void DisableNonExistentStatementIsNoOpTest() + { + List lines = + [ + "var x = ok", + "${x}" + ]; + + YesNtAssert.IsLastLineEqualWithSetup(lines, "ok", setup: interpreter => + { + interpreter.DisableStatement("nonexistent_keyword"); + }); + } + + [TestMethod] + public void EnableNonDisabledStatementIsNoOpTest() + { + List lines = + [ + "var x = ok", + "${x}" + ]; + + YesNtAssert.IsLastLineEqualWithSetup(lines, "ok", setup: interpreter => + { + interpreter.EnableStatement("var"); + }); + } + + [TestMethod] + public void AddStatementWithHighPriorityRunsBeforeNormalTest() + { + List lines = + [ + "priority_cmd arg" + ]; + + int callOrder = 0; + int highPriorityOrder = -1; + int normalPriorityOrder = -1; + + _ = YesNtAssert.GetLastLineWithSetup(lines, interpreter => + { + interpreter.AddStatement( + new StatementAttribute("priority_cmd", SearchMode.StartOfLine, SpaceAround.End) { Priority = Priority.High }, + _ => highPriorityOrder = callOrder++); + + interpreter.AddStatement( + new StatementAttribute("priority_cmd", SearchMode.StartOfLine, SpaceAround.End) { Priority = Priority.Normal }, + _ => normalPriorityOrder = callOrder++); + }); + + Assert.IsTrue(highPriorityOrder < normalPriorityOrder, "High priority statement should execute before Normal priority"); + } + + [TestMethod] + public void AddStatementAloneDoesNotReplaceBuiltinTest() + { + // Adding a custom 'var' handler without RemoveStatement first means BOTH handlers fire. + // The built-in still sets the variable, so ${x} resolves normally. + List lines = + [ + "var x = original", + "${x}" + ]; + + bool customHandlerCalled = false; + + YesNtAssert.IsLastLineEqualWithSetup(lines, "original", setup: interpreter => + { + interpreter.AddStatement("var", SearchMode.StartOfLine, SpaceAround.End, _ => + { + customHandlerCalled = true; + }); + }); + + Assert.IsTrue(customHandlerCalled, "Custom handler should have fired alongside the built-in"); + } + + [TestMethod] + public void RemoveThenAddStatementReplacesBuiltinTest() + { + // RemoveStatement + AddStatement correctly replaces the built-in. + // The built-in 'var' is gone, so only the custom handler fires. + List lines = + [ + "var x = original", + "${x}" + ]; + + YesNtAssert.ContainsTerminationMessageWithSetup(lines, "Variable \"x\" not found", interpreter => + { + interpreter.RemoveStatement("var"); + interpreter.AddStatement("var", SearchMode.StartOfLine, SpaceAround.End, _ => { }); + }); + } + + [TestMethod] + public void AddStatementWithRuntimeInfoCanSetVariableTest() + { + List lines = + [ + "set_var foo", + "${foo}" + ]; + + YesNtAssert.IsLastLineEqualWithSetup(lines, "hello", interpreter => + { + interpreter.AddStatement("set_var", SearchMode.StartOfLine, SpaceAround.End, (args, rt) => + { + rt.Variables[args] = "hello"; + }); + }); + } + + [TestMethod] + public void AddStatementWithRuntimeInfoCanReadVariableTest() + { + List lines = + [ + "var greeting = world", + "echo_var greeting" + ]; + + string? captured = null; + + YesNtAssert.GetLastLineWithSetup(lines, interpreter => + { + interpreter.AddStatement("echo_var", SearchMode.StartOfLine, SpaceAround.End, (args, rt) => + { + _ = rt.Variables.TryGetValue(args, out captured); + }); + }); + + Assert.AreEqual("world", captured); + } +} \ No newline at end of file diff --git a/YesNt.Interpreter.Tests/AssemblyInfo.cs b/YesNt.Interpreter.Tests/AssemblyInfo.cs new file mode 100644 index 0000000..38707ae --- /dev/null +++ b/YesNt.Interpreter.Tests/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.MethodLevel)] diff --git a/YesNt.Interpreter.Tests/CodeFlowStatementsTests.cs b/YesNt.Interpreter.Tests/CodeFlowStatementsTests.cs new file mode 100644 index 0000000..e74d48c --- /dev/null +++ b/YesNt.Interpreter.Tests/CodeFlowStatementsTests.cs @@ -0,0 +1,80 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System.Collections.Generic; + +namespace YesNt.Interpreter.Tests; + +[TestClass] +public class CodeFlowStatementsTests +{ + [TestMethod] + public void ExitStopsExecutionTest() + { + List lines = + [ + "var result = before", + "exit", + "var result = after", + "${result}" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Planned termination by code"); + } + + [TestMethod] + public void AbortAllStopsExecutionTest() + { + List lines = + [ + "abort_all", + "var result = after", + "${result}" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Canceling all tasks"); + } + + [TestMethod] + public void ThrowTerminatesWithErrorFlagTest() + { + List lines = + [ + "throw bad" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "with the message: bad"); + } + + [TestMethod] + public void ErrorTerminatesWithMessageTest() + { + List lines = + [ + "error soft" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "with the message: soft"); + } + + [TestMethod] + public void MissingLabelFailsTest() + { + List lines = + [ + "goto nowhere" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Label \"nowhere\" not found"); + } + + [TestMethod] + public void MissingFunctionFailsTest() + { + List lines = + [ + "call nowhere" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Function \"nowhere\" not found"); + } +} \ No newline at end of file diff --git a/YesNt.Interpreter.Tests/CodeFlowTests.cs b/YesNt.Interpreter.Tests/CodeFlowTests.cs new file mode 100644 index 0000000..66df648 --- /dev/null +++ b/YesNt.Interpreter.Tests/CodeFlowTests.cs @@ -0,0 +1,288 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System.Collections.Generic; + +namespace YesNt.Interpreter.Tests; + +[TestClass] +public class CodeFlowTests +{ + [TestMethod] + public void FunctionTest() + { + List lines = + [ + "call yes", + "func yes:", + "global result = 1", + "return", + "${result}" + ]; + YesNtAssert.IsLastLineEqual(lines, "1"); + } + + [TestMethod] + public void LabelsTest() + { + List lines = + [ + "var result = 1", + "goto yes", + "var result = 0", + "label yes:", + "${result}" + ]; + YesNtAssert.IsLastLineEqual(lines, "1"); + } + + [TestMethod] + public void IfBlockTrueTest() + { + List lines = + [ + "var result = low", + "if 6 > 5:", + "var result = high", + "end_if", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "high"); + } + + [TestMethod] + public void IfElseFalseBranchTest() + { + List lines = + [ + "var result = low", + "if 6 < 5:", + "var result = high", + "else:", + "var result = medium", + "end_if", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "medium"); + } + + [TestMethod] + public void NestedIfElseTest() + { + List lines = + [ + "var result = 0", + "if 1 == 1:", + "if 2 == 3:", + "var result = 1", + "else:", + "var result = 2", + "end_if", + "end_if", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "2"); + } + + [TestMethod] + public void WhileLoopTest() + { + List lines = + [ + "var i = 3", + "while ${i} > 0:", + "var i = ${i} - 1 calc", + "end_while", + "${i}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "0"); + } + + [TestMethod] + public void WhileSkipBodyWhenFalseTest() + { + List lines = + [ + "var i = 0", + "while ${i} > 0:", + "var i = 99", + "end_while", + "${i}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "0"); + } + + [TestMethod] + public void NestedWhileLoopTest() + { + List lines = + [ + "var outer = 2", + "var count = 0", + "while ${outer} > 0:", + "var inner = 2", + "while ${inner} > 0:", + "var count = ${count} + 1 calc", + "var inner = ${inner} - 1 calc", + "end_while", + "var outer = ${outer} - 1 calc", + "end_while", + "${count}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "4"); + } + + [TestMethod] + public void IfElseTrueSkipsElseBranchTest() + { + List lines = + [ + "var result = 0", + "if 2 > 1:", + "var result = 1", + "else:", + "var result = 2", + "end_if", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "1"); + } + + [TestMethod] + public void IfWithoutElseFalseSkipsBodyTest() + { + List lines = + [ + "var result = 5", + "if 1 == 2:", + "var result = 1", + "end_if", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "5"); + } + + [TestMethod] + public void IfGotoTrueTest() + { + List lines = + [ + "var result = 0", + "if 1 == 1 goto done", + "var result = 2", + "label done:", + "var result = ${result} + 1 calc", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "1"); + } + + [TestMethod] + public void IfCallTrueTest() + { + List lines = + [ + "func set_result:", + "global result = ok", + "return", + "if 1 == 1 call set_result", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "ok"); + } + + [TestMethod] + public void GotoInsideLoopExitsLoopTest() + { + List lines = + [ + "var hit = no", + "while 1 == 1:", + "var hit = yes", + "goto done", + "end_while", + "label done:", + "${hit}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "yes"); + } + + [TestMethod] + public void LabelWithoutColonFailsTest() + { + List lines = + [ + "label loop" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Invalid statement"); + } + + [TestMethod] + public void FunctionWithoutColonFailsTest() + { + List lines = + [ + "func missing" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Invalid statement"); + } + + [TestMethod] + public void MissingEndIfFailsTest() + { + List lines = + [ + "if 1 == 2:", + "var result = 1" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "No matching end_if found"); + } + + [TestMethod] + public void ElseWithoutIfFailsTest() + { + List lines = + [ + "else:" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "No matching end_if found"); + } + + [TestMethod] + public void MissingEndWhileFailsTest() + { + List lines = + [ + "var i = 0", + "while ${i} > 1:", + "var i = ${i} + 1 calc" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "No matching end_while found"); + } + + [TestMethod] + public void EndWhileWithoutWhileFailsTest() + { + List lines = + [ + "end_while" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "No matching while found"); + } +} \ No newline at end of file diff --git a/YesNt.Interpreter.Tests/CodeFowTests.cs b/YesNt.Interpreter.Tests/CodeFowTests.cs deleted file mode 100644 index 105b253..0000000 --- a/YesNt.Interpreter.Tests/CodeFowTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; - -using System.Collections.Generic; - -namespace YesNt.Interpreter.Tests; - -[TestClass] -public class CodeFlowTests -{ - [TestMethod] - public void FunctionTest() - { - List lines = new List() - { - "cal yes", - "fnc yes", - "!result" - }; - YesNtAssert.IsLastLineEqual(lines, "1"); - } - - [TestMethod] - public void LabelsTest() - { - List lines = new List() - { - "result" - }; - YesNtAssert.IsLastLineEqual(lines, "1"); - } - - [TestMethod] - public void CalculationsTest() - { - Assert.Inconclusive(); - YesNtAssert.IsLineEqual("10 * 10 !calc", (20).ToString()); - } -} \ No newline at end of file diff --git a/YesNt.Interpreter.Tests/ConsoleStatementsTests.cs b/YesNt.Interpreter.Tests/ConsoleStatementsTests.cs new file mode 100644 index 0000000..1ba7cbc --- /dev/null +++ b/YesNt.Interpreter.Tests/ConsoleStatementsTests.cs @@ -0,0 +1,110 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +using YesNt.Interpreter.Runtime; + +namespace YesNt.Interpreter.Tests; + +[TestClass] +public class ConsoleStatementsTests +{ + private static readonly object ConsoleLock = new object(); + + [TestMethod] + public void PrintLineWritesOutputTest() + { + List lines = + [ + "print_line hello" + ]; + + YesNtAssert.ContainsDebugOutput(lines, "hello"); + } + + [TestMethod] + public void PrintWritesOutputTest() + { + List lines = + [ + "print hello" + ]; + + YesNtAssert.ContainsDebugOutput(lines, "hello"); + } + + [TestMethod] + public void ClearThrowsInNonInteractiveConsoleTest() + { + List lines = + [ + "clear" + ]; + + _ = Assert.Throws(() => YesNtAssert.GetLastLine(lines)); + } + + [TestMethod] + public void ReadLineReplacesTokenTest() + { + lock (ConsoleLock) + { + TextReader originalIn = Console.In; + + try + { + Console.SetIn(new StringReader("typed value" + Environment.NewLine)); + + List lines = + [ + "var value = %read_line", + "${value}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "typed value"); + } + finally + { + Console.SetIn(originalIn); + } + } + } + + [TestMethod] + [DoNotParallelize] // timing-sensitive: relies on Thread.Sleep to let the interpreter reach %read_key + public void ReadKeyCanBeInterruptedByStopTest() + { + YesNtInterpreter interpreter = new YesNtInterpreter(); + + AutoResetEvent onDone = new AutoResetEvent(false); + StringBuilder output = new StringBuilder(); + + interpreter.OnDebugOutput += (s) => _ = output.Append(s); + interpreter.OnLineExecuted += (e) => + { + if (e is null) + { + _ = onDone.Set(); + } + }; + + List lines = + [ + "var value = %read_key" + ]; + + _ = Task.Run(() => interpreter.Execute(lines, true)); + + Thread.Sleep(100); + interpreter.Stop(); + + _ = onDone.WaitOne(TimeSpan.FromSeconds(3)); + + StringAssert.Contains(output.ToString(), "Terminated by external process"); + } +} \ No newline at end of file diff --git a/YesNt.Interpreter.Tests/FunctionStatementsTests.cs b/YesNt.Interpreter.Tests/FunctionStatementsTests.cs new file mode 100644 index 0000000..667ae4f --- /dev/null +++ b/YesNt.Interpreter.Tests/FunctionStatementsTests.cs @@ -0,0 +1,226 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System.Collections.Generic; + +namespace YesNt.Interpreter.Tests; + +[TestClass] +public class FunctionStatementsTests +{ + [TestMethod] + public void FunctionCallWithInParameterTest() + { + List lines = + [ + "goto main", + "func echo:", + "global result = %in", + "return", + "label main:", + "call echo with hello", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "hello"); + } + + [TestMethod] + public void HasInAndHasOutTokensTest() + { + List lines = + [ + "goto main", + "func probe:", + "global hasInBefore = %has_in", + "var consume = %in", + "global hasInAfter = %has_in", + "push_out ${hasInBefore}", + "push_out ${hasInAfter}", + "return", + "label main:", + "call probe with x", + "var hasOut = %has_out", + "${hasOut}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "True"); + } + + [TestMethod] + public void OutParameterReadTest() + { + List lines = + [ + "goto main", + "func make:", + "push_out out_value", + "return", + "label main:", + "call make with anything", + "var value = %out", + "${value}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "out_value"); + } + + [TestMethod] + public void OutParameterWithoutValueFailsTest() + { + List lines = + [ + "var x = %out" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "No out argument in stack"); + } + + [TestMethod] + public void InParameterOutsideFunctionFailsTest() + { + List lines = + [ + "var x = %in" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Statement not allowed outside of function"); + } + + [TestMethod] + public void ReturnOutsideFunctionFailsTest() + { + List lines = + [ + "return" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Statement not allowed outside of function"); + } + + [TestMethod] + public void PushOutOutsideFunctionFailsTest() + { + List lines = + [ + "push_out value" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Statement not allowed outside of function"); + } + + [TestMethod] + public void FunctionWithoutColonFailsTest() + { + List lines = + [ + "func missing" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Invalid statement"); + } + + [TestMethod] + public void NestedFunctionDefinitionFailsTest() + { + List lines = + [ + "func outer:", + "func inner:", + "return" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Nested functions are not allowed"); + } + + [TestMethod] + public void LocalVariableDoesNotLeakToCallerTest() + { + List lines = + [ + "goto main", + "func modify:", + "var x = inner", + "return", + "label main:", + "var x = outer", + "call modify", + "${x}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "outer"); + } + + [TestMethod] + public void ClearCallStackRunsTest() + { + List lines = + [ + "clear_call_stack", + "var result = ok", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "ok"); + } + + // --- Error path tests --- + + [TestMethod] + public void AccessInWithoutArgFailsTest() + { + List lines = + [ + "goto main", + "func noin:", + "var x = %in", + "return", + "label main:", + "call noin" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "No in argument in stack"); + } + + // --- Nested scope tests --- + + [TestMethod] + public void GlobalModifiedInsideFunctionIsVisibleAfterReturnTest() + { + List lines = + [ + "goto main", + "func setglobal:", + "global shared = modified", + "return", + "label main:", + "global shared = original", + "call setglobal", + "${shared}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "modified"); + } + + [TestMethod] + public void NestedFunctionCallsHaveIndependentLocalScopesTest() + { + List lines = + [ + "goto main", + "func outer:", + "var x = outer_val", + "call inner", + "push_out ${x}", + "return", + "func inner:", + "var x = inner_val", + "return", + "label main:", + "call outer", + "var result = %out", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "outer_val"); + } +} \ No newline at end of file diff --git a/YesNt.Interpreter.Tests/ListStatementsTests.cs b/YesNt.Interpreter.Tests/ListStatementsTests.cs new file mode 100644 index 0000000..e06f396 --- /dev/null +++ b/YesNt.Interpreter.Tests/ListStatementsTests.cs @@ -0,0 +1,161 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System.Collections.Generic; + +namespace YesNt.Interpreter.Tests; + +[TestClass] +public class ListStatementsTests +{ + [TestMethod] + public void ListCreateAddGetTest() + { + List lines = + [ + "list items new", + "list items add a", + "list items add b", + "list items get 1", + "var result = %out", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "b"); + } + + [TestMethod] + public void ListSetAndInsertTest() + { + List lines = + [ + "list items new", + "list items add a", + "list items add c", + "list items insert 1 b", + "list items set 2 d", + "list items get 2", + "var result = %out", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "d"); + } + + [TestMethod] + public void ListRemoveAndLengthTest() + { + List lines = + [ + "list items new", + "list items add a", + "list items add b", + "list items add c", + "list items remove 1", + "list items length", + "var len = %out", + "${len}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "2"); + } + + [TestMethod] + public void ListClearTest() + { + List lines = + [ + "list items new", + "list items add a", + "list items clear", + "list items length", + "var len = %out", + "${len}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "0"); + } + + [TestMethod] + public void ListDeleteTest() + { + List lines = + [ + "list items new", + "list items delete", + "list items length" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "List \"items\" not found"); + } + + [TestMethod] + public void ListMissingFailsTest() + { + List lines = + [ + "list missing get 0" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "List \"missing\" not found"); + } + + [TestMethod] + public void ListInvalidIndexFailsTest() + { + List lines = + [ + "list items new", + "list items add a", + "list items get 3" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Index 3 out of range"); + } + + [TestMethod] + public void ListInvalidSyntaxFailsTest() + { + List lines = + [ + "list items add" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Invalid statement"); + } + + [TestMethod] + public void ListScopeInsideFunctionTest() + { + List lines = + [ + "goto main", + "func make:", + "list items new", + "list items add x", + "list items get 0", + "push_out %out", + "return", + "label main:", + "call make", + "var result = %out", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "x"); + } + + [TestMethod] + public void ListAddWithSpacesTest() + { + List lines = + [ + "list items new", + "list items add \"hello world\"", + "list items get 0", + "var result = %out eval", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "hello world"); + } +} \ No newline at end of file diff --git a/YesNt.Interpreter.Tests/PredefinedVariableStatementsTests.cs b/YesNt.Interpreter.Tests/PredefinedVariableStatementsTests.cs new file mode 100644 index 0000000..4deb1b1 --- /dev/null +++ b/YesNt.Interpreter.Tests/PredefinedVariableStatementsTests.cs @@ -0,0 +1,107 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System; +using System.Collections.Generic; + +namespace YesNt.Interpreter.Tests; + +[TestClass] +public class PredefinedVariableStatementsTests +{ + [TestMethod] + public void TimeTokenProducesUnixTimestampTest() + { + List lines = + [ + "%time" + ]; + + string? value = YesNtAssert.GetLastLine(lines); + Assert.IsNotNull(value); + Assert.IsTrue(long.TryParse(value, out long parsed)); + + long now = DateTimeOffset.Now.ToUnixTimeSeconds(); + Assert.IsTrue(Math.Abs(now - parsed) < 10); + } + + [TestMethod] + public void OsTokenProducesValueTest() + { + List lines = + [ + "%os" + ]; + + string? value = YesNtAssert.GetLastLine(lines); + Assert.IsFalse(string.IsNullOrWhiteSpace(value)); + } + + [TestMethod] + public void CpuTokenProducesValueTest() + { + List lines = + [ + "%cpu" + ]; + + string? value = YesNtAssert.GetLastLine(lines); + Assert.IsFalse(string.IsNullOrWhiteSpace(value)); + } + + [TestMethod] + public void Is64TokenProducesBooleanTest() + { + List lines = + [ + "%is64" + ]; + + string? value = YesNtAssert.GetLastLine(lines); + Assert.AreEqual(Environment.Is64BitOperatingSystem.ToString(), value); + } + + [TestMethod] + public void PiTokenProducesPiTest() + { + List lines = + [ + "%pi" + ]; + + string? value = YesNtAssert.GetLastLine(lines); + Assert.IsNotNull(value); + Assert.IsTrue(double.TryParse(value, out double parsed)); + Assert.IsTrue(Math.Abs(parsed - Math.PI) < 0.001d); + } + + [TestMethod] + public void RandTokenProducesIntegerTest() + { + List lines = + [ + "%rand" + ]; + + string? value = YesNtAssert.GetLastLine(lines); + Assert.IsNotNull(value); + Assert.IsTrue(int.TryParse(value, out int parsed)); + Assert.IsTrue(parsed >= 32767); + } + + [TestMethod] + public void MultipleRandTokensAreReplacedTest() + { + List lines = + [ + "%rand %rand" + ]; + + string? value = YesNtAssert.GetLastLine(lines); + Assert.IsNotNull(value); + + string[] parts = value.Split(' ', StringSplitOptions.RemoveEmptyEntries); + Assert.AreEqual(2, parts.Length); + Assert.IsTrue(int.TryParse(parts[0], out _)); + Assert.IsTrue(int.TryParse(parts[1], out _)); + } +} \ No newline at end of file diff --git a/YesNt.Interpreter.Tests/ProcessingStatementsTests.cs b/YesNt.Interpreter.Tests/ProcessingStatementsTests.cs new file mode 100644 index 0000000..b3ec229 --- /dev/null +++ b/YesNt.Interpreter.Tests/ProcessingStatementsTests.cs @@ -0,0 +1,201 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System; +using System.Collections.Generic; +using System.IO; + +namespace YesNt.Interpreter.Tests; + +[TestClass] +public class ProcessingStatementsTests +{ + [TestMethod] + public void MultiplicationTest() + { + YesNtAssert.IsLineEqual("10 * 10 calc", "100"); + } + + [TestMethod] + public void DivisionTest() + { + YesNtAssert.IsLineEqual("90 / 4 calc", "22.5"); + } + + [TestMethod] + public void AdditionTest() + { + YesNtAssert.IsLineEqual("10 + 10 calc", "20"); + } + + [TestMethod] + public void SubtractionTest() + { + YesNtAssert.IsLineEqual("10 - 10 calc", "0"); + } + + [TestMethod] + public void ModulusTest() + { + YesNtAssert.IsLineEqual("10 % 3 calc", "1"); + } + + [TestMethod] + public void ExponentiationTest() + { + YesNtAssert.IsLineEqual("2 ^ 3 calc", "8"); + } + + [TestMethod] + public void EvalRawTildeSequenceIsLiteralTest() + { + YesNtAssert.IsLineEqual("hello~nliworld eval", "hello~nliworld"); + } + + [TestMethod] + public void EvalDecodesStringLiteralEscapesTest() + { + List lines = + [ + "var x = \"hello\\nworld\"", + "${x} eval" + ]; + + YesNtAssert.IsLastLineEqual(lines, "hello\nworld"); + } + + [TestMethod] + public void CalcRespectsPrecedenceTest() + { + YesNtAssert.IsLineEqual("2 + 3 * 4 calc", "14"); + } + + [TestMethod] + public void CalcParenthesesOverridePrecedenceTest() + { + YesNtAssert.IsLineEqual("(2 + 3) * 4 calc", "20"); + } + + [TestMethod] + public void SleepZeroIsValidTest() + { + List lines = + [ + "sleep 0", + "var result = ok", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "ok"); + } + + [TestMethod] + public void SleepInvalidValueFailsTest() + { + List lines = + [ + "sleep nope" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "\"nope\" is not a valid time-out value"); + } + + [TestMethod] + public void SleepRunsAndContinuesTest() + { + List lines = + [ + "sleep 5", + "var result = ok", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "ok"); + } + + [TestMethod] + public void LengthPushesOutParameterTest() + { + List lines = + [ + "length hello", + "var value = %out", + "${value}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "5"); + } + + [TestMethod] + public void ImportLoadsScriptTest() + { + string tempFile = Path.Combine(Path.GetTempPath(), $"yesnt-import-{Guid.NewGuid():N}.ynt"); + + try + { + File.WriteAllText(tempFile, "var imported = yes"); + + List lines = + [ + $"import {tempFile}", + "${imported}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "yes"); + } + finally + { + if (File.Exists(tempFile)) + { + File.Delete(tempFile); + } + } + } + + [TestMethod] + public void ImportMissingFileFailsTest() + { + List lines = + [ + $"import {Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))}.ynt" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Could not find file"); + } + + [TestMethod] + public void TaskCanUpdateGlobalVariableTest() + { + List lines = + [ + "global result = 0", + "global result = 1 task", + "while ${result} == 0:", + "sleep 10", + "end_while", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "1", timeout: 3000); + } + + [TestMethod] + public void MultipleTasksRunConcurrentlyTest() + { + List lines = + [ + "global a = 0", + "global b = 0", + "global a = 1 task", + "global b = 2 task", + "while ${a} == 0:", + "sleep 10", + "end_while", + "while ${b} == 0:", + "sleep 10", + "end_while", + "${b}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "2", timeout: 3000); + } +} \ No newline at end of file diff --git a/YesNt.Interpreter.Tests/StringLiteralStatementsTests.cs b/YesNt.Interpreter.Tests/StringLiteralStatementsTests.cs new file mode 100644 index 0000000..3402b69 --- /dev/null +++ b/YesNt.Interpreter.Tests/StringLiteralStatementsTests.cs @@ -0,0 +1,113 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System.Collections.Generic; + +namespace YesNt.Interpreter.Tests; + +[TestClass] +public class StringLiteralStatementsTests +{ + [TestMethod] + public void StringLiteralWithSpacesWorksTest() + { + List lines = + [ + "var msg = \"hello world\"", + "${msg}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "hello world"); + } + + [TestMethod] + public void StringLiteralEscapesWorkTest() + { + List lines = + [ + "var msg = \"a\\n\\t\\\"b\"", + "${msg}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "a\n\t\"b"); + } + + [TestMethod] + public void StringLiteralPreventsVariableInterpolationTest() + { + List lines = + [ + "var x = hidden", + "print_line \"${x}\"" + ]; + + YesNtAssert.ContainsDebugOutput(lines, "${x}"); + } + + [TestMethod] + public void StringLiteralWorksWithListAddTest() + { + List lines = + [ + "list items new", + "list items add \"hello world\"", + "list items get 0", + "var result = %out", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "hello world"); + } + + [TestMethod] + public void EmptyStringLiteralHasLengthZeroTest() + { + List lines = + [ + "var x = \"\"", + "length ${x}", + "var len = %out", + "${len}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "0"); + } + + [TestMethod] + public void EscapedBackslashProducesLiteralBackslashTest() + { + List lines = + [ + "var msg = \"\\\\n\"", // YesNt source: "\\n" → \n (backslash + n, 2 chars) + "length ${msg}", + "var len = %out", + "${len}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "2"); + } + + [TestMethod] + public void UnknownEscapeSequenceBecomesCharTest() + { + List lines = + [ + "var msg = \"\\q\"", // YesNt source: "\q" → q (1 char) + "length ${msg}", + "var len = %out", + "${len}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "1"); + } + + [TestMethod] + public void UnterminatedStringLiteralFailsTest() + { + List lines = + [ + "var msg = \"hello" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Invalid string literal"); + } +} \ No newline at end of file diff --git a/YesNt.Interpreter.Tests/SystemStatementsTests.cs b/YesNt.Interpreter.Tests/SystemStatementsTests.cs new file mode 100644 index 0000000..d8f4138 --- /dev/null +++ b/YesNt.Interpreter.Tests/SystemStatementsTests.cs @@ -0,0 +1,48 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System.Collections.Generic; + +namespace YesNt.Interpreter.Tests; + +[TestClass] +public class SystemStatementsTests +{ + [TestMethod] + public void ExecWithArgsRunsProcessTest() + { + List lines = + [ + "exec cmd with /c,echo yesnt", + "var exitCode = %out", + "${exitCode}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "0"); + } + + [TestMethod] + public void ExecWithInStackArgsRunsProcessTest() + { + List lines = + [ + "push_in /c", + "push_in echo yesnt", + "exec cmd", + "var exitCode = %out", + "${exitCode}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "0"); + } + + [TestMethod] + public void ExecInvalidProgramFailsTest() + { + List lines = + [ + "exec does_not_exist_abc_xyz" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Failed to start \"does_not_exist_abc_xyz\""); + } +} \ No newline at end of file diff --git a/YesNt.Interpreter.Tests/VariableStatementsTests.cs b/YesNt.Interpreter.Tests/VariableStatementsTests.cs new file mode 100644 index 0000000..540e467 --- /dev/null +++ b/YesNt.Interpreter.Tests/VariableStatementsTests.cs @@ -0,0 +1,131 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System.Collections.Generic; + +namespace YesNt.Interpreter.Tests; + +[TestClass] +public class VariableStatementsTests +{ + [TestMethod] + public void LetAndReadVariableTest() + { + List lines = + [ + "var value = hi", + "${value}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "hi"); + } + + [TestMethod] + public void GlobalVariableReadTest() + { + List lines = + [ + "global value = hi", + "${value}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "hi"); + } + + [TestMethod] + public void LocalVariableOverridesGlobalTest() + { + List lines = + [ + "global value = global", + "var value = local", + "${value}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "local"); + } + + [TestMethod] + public void DeleteLocalVariableTest() + { + List lines = + [ + "var value = a", + "delete value", + "global value = b", + "${value}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "b"); + } + + [TestMethod] + public void DeleteMissingVariableFailsTest() + { + List lines = + [ + "delete missing" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Variable \"missing\" not found"); + } + + [TestMethod] + public void LetInvalidSyntaxFailsTest() + { + List lines = + [ + "var a" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Invalid statement"); + } + + [TestMethod] + public void LetInvalidNameFailsTest() + { + List lines = + [ + "var a b = 1" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Invalid syntax"); + } + + [TestMethod] + public void OverwriteVariableTest() + { + List lines = + [ + "var x = first", + "var x = second", + "${x}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "second"); + } + + [TestMethod] + public void EmptyStringVariableHasLengthZeroTest() + { + List lines = + [ + "var x = \"\"", + "length ${x}", + "var len = %out", + "${len}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "0"); + } + + [TestMethod] + public void MissingVariableReferenceFailsTest() + { + List lines = + [ + "${missing}" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Variable \"missing\" not found"); + } +} \ No newline at end of file diff --git a/YesNt.Interpreter.Tests/YesNt.Interpreter.Tests.csproj b/YesNt.Interpreter.Tests/YesNt.Interpreter.Tests.csproj index cf7cd15..592c72d 100644 --- a/YesNt.Interpreter.Tests/YesNt.Interpreter.Tests.csproj +++ b/YesNt.Interpreter.Tests/YesNt.Interpreter.Tests.csproj @@ -1,17 +1,22 @@ - net6.0 + net10.0 enable false + + AnyCPU;x64 - - - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/YesNt.Interpreter.Tests/YesNtAssert.cs b/YesNt.Interpreter.Tests/YesNtAssert.cs index e7ae5f7..67ee9d5 100644 --- a/YesNt.Interpreter.Tests/YesNtAssert.cs +++ b/YesNt.Interpreter.Tests/YesNtAssert.cs @@ -1,7 +1,9 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.VisualStudio.TestTools.UnitTesting; using System; using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; using System.Threading; using YesNt.Interpreter.Runtime; @@ -10,54 +12,115 @@ namespace YesNt.Interpreter.Tests; internal static class YesNtAssert { - private static readonly YesNtInterpreter yesNtInterpreter = new YesNtInterpreter(); - - static YesNtAssert() - { - yesNtInterpreter.Initialize(); - } - public static void IsLastLineEqual(List lines, string expected, int timeout = 1000) { - AutoResetEvent onDone = new AutoResetEvent(false); - - DebugEventArgs debugEventArgs = new DebugEventArgs(); - yesNtInterpreter.OnLineExecuted += (er) => - { - debugEventArgs = er ?? debugEventArgs; - - if (er is null) - { - onDone.Set(); - } - }; - - yesNtInterpreter.Execute(lines, true); - - _ = onDone.WaitOne(TimeSpan.FromSeconds(timeout)); - - Assert.AreEqual(expected, debugEventArgs.CurrentLine); + (DebugEventArgs? debugEventArgs, _) = ExecuteAndCapture(lines, timeout); + Assert.AreEqual(expected, debugEventArgs?.CurrentLine); } public static void IsLineEqual(string line, string expected, int timeout = 1000) { - AutoResetEvent onDone = new AutoResetEvent(false); - List lines = new List() - { + List lines = + [ line - }; + ]; + + (DebugEventArgs? debugEventArgs, _) = ExecuteAndCapture(lines, timeout); + Assert.AreEqual(expected, debugEventArgs?.CurrentLine); + } + + public static void IsLineNotEqual(string line, string expected, int timeout = 1000) + { + List lines = + [ + line + ]; + + (DebugEventArgs? debugEventArgs, _) = ExecuteAndCapture(lines, timeout); + Assert.AreNotEqual(expected, debugEventArgs?.CurrentLine); + } + + public static void ContainsTerminationMessage(List lines, string expectedMessageFragment, int timeout = 1000) + { + (_, string debugOutput) = ExecuteAndCapture(lines, timeout); + + StringAssert.Contains(debugOutput, expectedMessageFragment); + } + + public static void ContainsDebugOutput(List lines, string expectedFragment, int timeout = 1000) + { + (_, string debugOutput) = ExecuteAndCapture(lines, timeout); + + StringAssert.Contains(debugOutput, expectedFragment); + } + + public static string? GetLastLine(List lines, int timeout = 1000) + { + (DebugEventArgs? debugEventArgs, _) = ExecuteAndCapture(lines, timeout); + return debugEventArgs?.CurrentLine; + } + + public static void LastLineMatches(List lines, string pattern, int timeout = 1000) + { + string? value = GetLastLine(lines, timeout); + Assert.IsNotNull(value); + StringAssert.Matches(value, new Regex(pattern)); + } + + public static void IsLastLineEqualWithSetup(List lines, string expected, Action setup, int timeout = 1000) + { + (DebugEventArgs? debugEventArgs, _) = ExecuteAndCapture(lines, timeout, setup); + Assert.AreEqual(expected, debugEventArgs?.CurrentLine); + } + + public static void ContainsDebugOutputWithSetup(List lines, string expectedFragment, Action setup, int timeout = 1000) + { + (_, string debugOutput) = ExecuteAndCapture(lines, timeout, setup); + StringAssert.Contains(debugOutput, expectedFragment); + } + + public static void ContainsTerminationMessageWithSetup(List lines, string expectedMessageFragment, Action setup, int timeout = 1000) + { + (_, string debugOutput) = ExecuteAndCapture(lines, timeout, setup); + StringAssert.Contains(debugOutput, expectedMessageFragment); + } + + public static string? GetLastLineWithSetup(List lines, Action setup, int timeout = 1000) + { + (DebugEventArgs? debugEventArgs, _) = ExecuteAndCapture(lines, timeout, setup); + return debugEventArgs?.CurrentLine; + } + + private static (DebugEventArgs? LastDebugEvent, string DebugOutput) ExecuteAndCapture(List lines, int timeout, Action? setup = null) + { + YesNtInterpreter yesNtInterpreter = new YesNtInterpreter(); + setup?.Invoke(yesNtInterpreter); + + AutoResetEvent onDone = new AutoResetEvent(false); + DebugEventArgs? debugEventArgs = null; + StringBuilder outputBuilder = new StringBuilder(); - DebugEventArgs debugEventArgs = new DebugEventArgs(); yesNtInterpreter.OnLineExecuted += (er) => { - debugEventArgs = er ?? debugEventArgs; - onDone.Set(); + if (er is not null) + { + debugEventArgs = er; + } + else + { + _ = onDone.Set(); + } + }; + + yesNtInterpreter.OnDebugOutput += (s) => + { + _ = outputBuilder.Append(s); }; yesNtInterpreter.Execute(lines, true); _ = onDone.WaitOne(TimeSpan.FromMilliseconds(timeout)); - Assert.AreEqual(expected, debugEventArgs.CurrentLine); + return (debugEventArgs, outputBuilder.ToString()); } } \ No newline at end of file diff --git a/YesNt.Interpreter/Attributes/StatementAttribute.cs b/YesNt.Interpreter/Attributes/StatementAttribute.cs index 9b55c9b..24f675a 100644 --- a/YesNt.Interpreter/Attributes/StatementAttribute.cs +++ b/YesNt.Interpreter/Attributes/StatementAttribute.cs @@ -1,36 +1,110 @@ -using System; +using System; using YesNt.Interpreter.Enums; -namespace YesNt.Interpreter.Attributes +namespace YesNt.Interpreter.Attributes; + +/// +/// Marks a method as a YesNt statement handler. +/// The interpreter matches source lines against the keyword according to +/// and rules, then invokes the decorated method +/// with the remaining argument text. +/// +/// +/// Methods decorated with this attribute must be instance methods on a class that inherits +/// and must accept a single parameter. +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public class StatementAttribute : Attribute { - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - internal class StatementAttribute : Attribute + /// Gets the keyword that identifies this statement in source code. + public string Name { get; } + + /// Gets where in the line the keyword is searched for. + public SearchMode SearchMode { get; } + + /// Gets which sides of the keyword must be padded with a space. + public SpaceAround SpaceAround { get; } + + /// Gets or sets the syntax-highlight color used by the code editor. + public ConsoleColor Color { get; set; } + + /// + /// Gets or sets the execution priority. Statements with a lower value + /// run before those with a higher value. Defaults to . + /// + public Priority Priority { get; set; } = Priority.Normal; + + /// + /// Gets or sets a value indicating whether this statement is still invoked while the interpreter + /// is in search mode (scanning for a label or function definition). Defaults to . + /// + public bool ExecuteInSearchMode { get; set; } + + /// + /// Gets or sets a value indicating whether the full current line (including the keyword itself) + /// is passed as the argument, rather than stripping the keyword prefix/suffix first. + /// Defaults to . + /// + public bool KeepStatementInArgs { get; set; } + + /// + /// Gets a value indicating whether this statement should be excluded from syntax highlighting. + /// Set to when no is provided. + /// + public bool IgnoreSyntaxHighlighting { get; } + + /// + /// Gets or sets an optional sub-string that must also be present in the line for this statement + /// to match. Used to differentiate overloaded keywords (e.g. call vs call … with …). + /// + public string Separator { get; set; } + + /// + /// Gets or sets the name of the statement that marks the end of this block. + /// Used for block boundary caching (e.g., "while" has BlockPair = "end_while"). + /// + public string BlockPair { get; set; } + + /// + /// Gets or sets a value indicating whether this statement is the end of a block. + /// Used for block boundary caching (e.g., "end_while" has IsBlockEnd = true). + /// + public bool IsBlockEnd { get; set; } + + /// + /// Gets or sets a value indicating whether this statement is an intermediate part of a block + /// (e.g., "else:" between "if" and "end_if"). + /// + public bool IsBlockIntermediate { get; set; } + + /// + /// Initializes a new with a syntax-highlight color. + /// + /// The keyword that identifies this statement. + /// 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. + public StatementAttribute(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor color) { - public string Name { get; } - public SearchMode SearchMode { get; } - public SpaceAround SpaceAround { get; } - public ConsoleColor Color { get; set; } - public Priority Priority { get; set; } = Priority.Normal; - public bool ExecuteInSearchMode { get; set; } - public bool KeepStatementInArgs { get; set; } - public bool IgnoreSyntaxHighlighting { get; } - public string Seperator { get; set; } + Name = name; + SearchMode = searchMode; + SpaceAround = spaceAround; + Color = color; + } - internal StatementAttribute(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor color) - { - Name = name; - SearchMode = searchMode; - SpaceAround = spaceAround; - Color = color; - } - - internal StatementAttribute(string name, SearchMode searchMode, SpaceAround spaceAround) - { - Name = name; - SearchMode = searchMode; - SpaceAround = spaceAround; - IgnoreSyntaxHighlighting = true; - } + /// + /// Initializes a new without a syntax-highlight color. + /// The statement will be excluded from syntax highlighting. + /// + /// The keyword that identifies this statement. + /// Where in the line the keyword is matched. + /// Which sides of the keyword require a surrounding space. + public StatementAttribute(string name, SearchMode searchMode, SpaceAround spaceAround) + { + Name = name; + SearchMode = searchMode; + SpaceAround = spaceAround; + IgnoreSyntaxHighlighting = true; } } \ No newline at end of file diff --git a/YesNt.Interpreter/Attributes/StaticStatementAttribute.cs b/YesNt.Interpreter/Attributes/StaticStatementAttribute.cs index 28bf029..4ed6682 100644 --- a/YesNt.Interpreter/Attributes/StaticStatementAttribute.cs +++ b/YesNt.Interpreter/Attributes/StaticStatementAttribute.cs @@ -2,12 +2,30 @@ using YesNt.Interpreter.Enums; -namespace YesNt.Interpreter.Attributes +namespace YesNt.Interpreter.Attributes; + +/// +/// Marks a parameterless method as a YesNt static statement handler. +/// Static statements are invoked once per line before regular statement matching begins, +/// regardless of whether the line matches any keyword. They are typically used for +/// pre-processing tasks such as transforming the current line before other statements run. +/// +/// +/// Methods decorated with this attribute must be instance methods on a class that inherits +/// and must have no parameters. +/// +[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] +public class StaticStatementAttribute : Attribute { - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] - internal class StaticStatementAttribute : Attribute - { - public bool ExecuteInSearchMode { get; set; } - public Priority Priority { get; set; } = Priority.Normal; - } + /// + /// Gets or sets a value indicating whether this handler is still invoked while the interpreter + /// is in search mode (scanning for a label or function definition). Defaults to . + /// + public bool ExecuteInSearchMode { get; set; } + + /// + /// Gets or sets the execution priority relative to other static statements. + /// Defaults to . + /// + public Priority Priority { get; set; } = Priority.Normal; } \ No newline at end of file diff --git a/YesNt.Interpreter/Enums/Priority.cs b/YesNt.Interpreter/Enums/Priority.cs index a65051a..8fbaf51 100644 --- a/YesNt.Interpreter/Enums/Priority.cs +++ b/YesNt.Interpreter/Enums/Priority.cs @@ -1,13 +1,28 @@ -namespace YesNt.Interpreter.Enums +namespace YesNt.Interpreter.Enums; + +/// +/// Controls the execution order of statements. Lower values run first. +/// +public enum Priority { - internal enum Priority - { - PreProcessing, - Highest, - VeryHigh, - High, - Normal, - Low, - VeryLow - } + /// Runs before all other statements. Used for syntax pre-processing such as string literals. + PreProcessing, + + /// Runs very early. Used for inline substitutions such as variable reads and parameter pops. + Highest, + + /// Runs early. + VeryHigh, + + /// Runs above normal order. + High, + + /// Default execution order. + Normal, + + /// Runs below normal order. + Low, + + /// Runs last. Used for control-flow and variable definitions that depend on substitutions being complete. + VeryLow } \ No newline at end of file diff --git a/YesNt.Interpreter/Enums/SearchMode.cs b/YesNt.Interpreter/Enums/SearchMode.cs index 0e083b8..a13e749 100644 --- a/YesNt.Interpreter/Enums/SearchMode.cs +++ b/YesNt.Interpreter/Enums/SearchMode.cs @@ -1,10 +1,19 @@ -namespace YesNt.Interpreter.Enums +namespace YesNt.Interpreter.Enums; + +/// +/// Determines where in a source line the interpreter searches for a statement keyword. +/// +public enum SearchMode { - public enum SearchMode - { - StartOfLine, - EndOfLine, - Contains, - Exact - } + /// The keyword must appear at the beginning of the line. + StartOfLine, + + /// The keyword must appear at the end of the line. + EndOfLine, + + /// The keyword may appear anywhere in the line. + Contains, + + /// The entire line must exactly match the keyword. + Exact } \ No newline at end of file diff --git a/YesNt.Interpreter/Enums/SpaceAround.cs b/YesNt.Interpreter/Enums/SpaceAround.cs index c16fd85..c9e4292 100644 --- a/YesNt.Interpreter/Enums/SpaceAround.cs +++ b/YesNt.Interpreter/Enums/SpaceAround.cs @@ -1,10 +1,19 @@ -namespace YesNt.Interpreter.Enums +namespace YesNt.Interpreter.Enums; + +/// +/// Specifies which sides of a statement keyword must be surrounded by a space when matching. +/// +public enum SpaceAround { - public enum SpaceAround - { - StartEnd, - Start, - End, - None - } + /// A space is required both before and after the keyword. + StartEnd, + + /// A space is required before the keyword only. + Start, + + /// A space is required after the keyword only. + End, + + /// No surrounding spaces are required. + None } \ No newline at end of file diff --git a/YesNt.Interpreter/Program.cs b/YesNt.Interpreter/Program.cs deleted file mode 100644 index 70ce45e..0000000 --- a/YesNt.Interpreter/Program.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; - -using YesNt.Interpreter.Runtime; - -namespace YesNt.Interpreter -{ - internal static class Program - { - private static void Main(string[] args) - { - if (args.Length == 1) - { - YesNtInterpreter interpreter = new YesNtInterpreter(); - interpreter.Initialize(); - interpreter.Execute(args[0]); - } - else - { - Console.WriteLine("No path specified!"); - } - } - } -} \ No newline at end of file diff --git a/YesNt.Interpreter/Properties/launchSettings.json b/YesNt.Interpreter/Properties/launchSettings.json index f380490..60b7039 100644 --- a/YesNt.Interpreter/Properties/launchSettings.json +++ b/YesNt.Interpreter/Properties/launchSettings.json @@ -1,13 +1,13 @@ { - "profiles": { - "YesNt-Interpreter": { - "commandName": "Project", - "commandLineArgs": "code.ynt" - }, - "WSL": { - "commandName": "WSL2", - "environmentVariables": {}, - "distributionName": "" + "profiles": { + "YesNt-Interpreter": { + "commandName": "Project", + "commandLineArgs": "code.ynt" + }, + "WSL": { + "commandName": "WSL2", + "environmentVariables": {}, + "distributionName": "" + } } - } } \ No newline at end of file diff --git a/YesNt.Interpreter/Runtime/DebugEventArgs.cs b/YesNt.Interpreter/Runtime/DebugEventArgs.cs index 798d860..0a0a842 100644 --- a/YesNt.Interpreter/Runtime/DebugEventArgs.cs +++ b/YesNt.Interpreter/Runtime/DebugEventArgs.cs @@ -1,13 +1,33 @@ using System; -namespace YesNt.Interpreter.Runtime +namespace YesNt.Interpreter.Runtime; + +/// +/// Provides per-line execution data raised through . +/// +public class DebugEventArgs : EventArgs { - public class DebugEventArgs : EventArgs - { - public int LineNumber { get; internal set; } - public string CurrentLine { get; internal set; } - public string OriginalLine { get; internal set; } - public int TaskId { get; internal set; } - public bool IsTask { get; internal set; } - } + /// Gets the 1-based line number of the executed line within its source file. + public int LineNumber { get; internal set; } + + /// + /// Gets the line content after all statement transformations have been applied + /// (e.g. after variable substitution). May differ from . + /// + public string CurrentLine { get; internal set; } + + /// Gets the raw line content as it appeared in the source file. + public string OriginalLine { get; internal set; } + + /// + /// Gets the task identifier of the task that executed this line, or 0 if the line + /// was executed on the main thread. + /// + public int TaskId { get; internal set; } + + /// + /// Gets a value indicating whether this line was executed inside a background task + /// (spawned with the task statement). + /// + public bool IsTask { get; internal set; } } \ No newline at end of file diff --git a/YesNt.Interpreter/Runtime/ExitMessages.cs b/YesNt.Interpreter/Runtime/ExitMessages.cs new file mode 100644 index 0000000..44591c8 --- /dev/null +++ b/YesNt.Interpreter/Runtime/ExitMessages.cs @@ -0,0 +1,83 @@ +namespace YesNt.Interpreter.Runtime; + +/// +/// Central repository of all exit/error message strings used by . +/// Keeping messages here ensures consistency and makes them easy to find or localise. +/// +internal static class ExitMessages +{ + internal const string InvalidSyntax = "Invalid syntax"; + internal const string InvalidSyntaxColonRequired = "Invalid syntax. Statement must end with ':'"; + internal const string InvalidOperation = "Invalid operation"; + internal const string InvalidStatement = "Invalid statement"; + internal const string InvalidStringLiteral = "Invalid string literal"; + internal const string EndOfFile = "End of file"; + internal const string TerminatedByExternalProcess = "Terminated by external process"; + internal const string TerminatedByChildTask = "Terminated by child task"; + internal const string TerminatedByParentTask = "Terminated by parent task"; + internal const string PlannedTermination = "Planned termination by code"; + internal const string PlannedTerminationCancelingTasks = "Planned termination by code. Canceling all tasks"; + internal const string NoMatchingEndIf = "No matching end_if found"; + internal const string NoMatchingEndWhile = "No matching end_while found"; + internal const string NoMatchingWhile = "No matching while found"; + internal const string NestedFunctionsNotAllowed = "Nested functions are not allowed"; + internal const string NoOutArgumentInStack = "No out argument in stack"; + internal const string StatementNotAllowedOutsideFunction = "Statement not allowed outside of function"; + internal const string NoInArgumentInStack = "No in argument in stack"; + internal const string NoFunctionInStack = "No function in stack"; + + internal static string LabelNotFound(string label) + { + return $"Label \"{label}\" not found"; + } + + internal static string FunctionNotFound(string function) + { + return $"Function \"{function}\" not found"; + } + + internal static string VariableNotFound(string variable) + { + return $"Variable \"{variable}\" not found"; + } + + internal static string ListNotFound(string list) + { + return $"List \"{list}\" not found"; + } + + internal static string InvalidIndex(string rawIndex) + { + return $"\"{rawIndex}\" is not a valid index"; + } + + internal static string IndexOutOfRange(int index) + { + return $"Index {index} out of range"; + } + + internal static string InvalidTimeoutValue(string value) + { + return $"\"{value}\" is not a valid time-out value"; + } + + internal static string CouldNotLoadFile(string path) + { + return $"Could not load file \"{path}\""; + } + + internal static string CouldNotFindFile(string path) + { + return $"Could not find file \"{path}\""; + } + + internal static string CannotFindFile(string path) + { + return $"Cannot find file \"{path}\"."; + } + + internal static string FailedToStart(string program, string message) + { + return $"Failed to start \"{program}\". {message}"; + } +} \ No newline at end of file diff --git a/YesNt.Interpreter/Runtime/FunctionScope.cs b/YesNt.Interpreter/Runtime/FunctionScope.cs index ed9acf9..8203b46 100644 --- a/YesNt.Interpreter/Runtime/FunctionScope.cs +++ b/YesNt.Interpreter/Runtime/FunctionScope.cs @@ -1,19 +1,28 @@ using System.Collections.Generic; -namespace YesNt.Interpreter.Runtime -{ - internal class FunctionScope - { - public int CallerLine { get; } - public Dictionary Variables { get; } = new(); - public Dictionary Labels { get; } = new(); - public Stack Arguemtns { get; } - public Stack Results { get; } = new(); +namespace YesNt.Interpreter.Runtime; - public FunctionScope(int callerLine, Stack arguemtns) - { - CallerLine = callerLine; - Arguemtns = arguemtns; - } - } +/// +/// Represents one frame on the function call stack. Created when a call statement is +/// executed and popped when the matching return is reached. +/// +internal class FunctionScope(int callerLine, Stack arguments) +{ + /// Gets the zero-based line index to return to after this function completes. + public int CallerLine { get; } = callerLine; + + /// Gets the local variable table for this function invocation. + public Dictionary Variables { get; } = []; + + /// Gets the local list table for this function invocation. + public Dictionary> Lists { get; } = []; + + /// Gets the local label table for this function invocation. + public Dictionary Labels { get; } = []; + + /// Gets the stack of input arguments passed to this function via push_in. + public Stack Arguments { get; } = arguments; + + /// Gets the stack of output values pushed via push_out, consumed by the caller via %out. + public Stack Results { get; } = new(); } \ No newline at end of file diff --git a/YesNt.Interpreter/Runtime/IStatementContext.cs b/YesNt.Interpreter/Runtime/IStatementContext.cs new file mode 100644 index 0000000..58569e8 --- /dev/null +++ b/YesNt.Interpreter/Runtime/IStatementContext.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace YesNt.Interpreter.Runtime; + +/// +/// Exposes the script runtime state accessible to custom statement handlers registered +/// via . +/// +public interface IStatementContext +{ + /// Gets the local variable table for the current scope. + Dictionary Variables { get; } + + /// Gets or sets the global variable table shared across all scopes. + Dictionary GlobalVariables { get; set; } + + /// Gets or sets the text of the line currently being processed. + /// Inline-substitution handlers (e.g. %read_line) write their result here. + string CurrentLine { get; set; } + + /// Gets or sets the zero-based index of the next line to execute. + /// Set this to implement control-flow jumps inside a custom statement. + int LineNumber { get; set; } + + /// Terminates execution with the given message. + /// The message written to debug output. + /// + /// to signal an error termination; + /// for a planned, non-error termination. + /// + void Exit(string message, bool isError); +} diff --git a/YesNt.Interpreter/Runtime/Line.cs b/YesNt.Interpreter/Runtime/Line.cs index 4243dac..fcb53af 100644 --- a/YesNt.Interpreter/Runtime/Line.cs +++ b/YesNt.Interpreter/Runtime/Line.cs @@ -1,16 +1,16 @@ -namespace YesNt.Interpreter.Runtime -{ - internal class Line - { - public Line(string content, string fileName, int lineNumber) - { - Content = content; - FileName = fileName; - LineNumber = lineNumber; - } +namespace YesNt.Interpreter.Runtime; - public string Content { get; set; } - public string FileName { get; set; } - public int LineNumber { get; set; } - } +/// +/// Represents a single source line together with its location metadata. +/// +internal class Line(string content, string fileName, int lineNumber) +{ + /// Gets or sets the raw text content of the line. + public string Content { get; set; } = content; + + /// Gets or sets the name of the source file this line originated from. + public string FileName { get; set; } = fileName; + + /// Gets or sets the zero-based line index within . + public int LineNumber { get; set; } = lineNumber; } \ No newline at end of file diff --git a/YesNt.Interpreter/Runtime/RuntimeInformation.cs b/YesNt.Interpreter/Runtime/RuntimeInformation.cs index 0833eb0..0ac8db9 100644 --- a/YesNt.Interpreter/Runtime/RuntimeInformation.cs +++ b/YesNt.Interpreter/Runtime/RuntimeInformation.cs @@ -1,207 +1,203 @@ using System; using System.Collections.Generic; -using System.Linq; +using System.Threading; using YesNt.Interpreter.Utilities; -namespace YesNt.Interpreter.Runtime +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 { - internal class RuntimeInformation + 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 { - private RuntimeInformation parentRuntimeInformation; - private static int internalTaskId = 0; - private int taskId = 0; + get => InternalIsInFunction || FunctionCallStack.Count > 0; + set => InternalIsInFunction = value; + } - private readonly Dictionary topVariables = new(); - private readonly Dictionary topLabels = new(); + public Dictionary Variables => FunctionCallStack.Count == 0 ? topVariables : FunctionCallStack.Peek().Variables; + public Dictionary> Lists => FunctionCallStack.Count == 0 ? topLists : FunctionCallStack.Peek().Lists; - public Dictionary GloablVariables { get; set; } = new(); - public Dictionary Functions { get; } = new(); - public Stack FunctionCallStack { get; } = new(); - public Stack InParametersStack { get; } = new(); - public Stack OutParametersStack { get; set; } = new(); - public List Lines { get; set; } = new(); - 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 CurrentFilePath { get; set; } = string.Empty; - public bool IsTask => ParentRuntimeInformation is not null; - public int TaskId => IsTask ? taskId : 0; - public bool InternalIsInFunction { get; set; } + public Dictionary Labels { get => FunctionCallStack.Count == 0 ? field : FunctionCallStack.Peek().Labels; } = []; - public bool IsInFunction + public RuntimeInformation ParentRuntimeInformation + { + get; + set { - get => InternalIsInFunction || FunctionCallStack.Count > 0; - set => InternalIsInFunction = value; + 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; } - public Dictionary Variables - { - get - { - if (FunctionCallStack.Count == 0) - { - return topVariables; - } - else - { - return FunctionCallStack.Peek().Variables; - } - } - } - - public Dictionary Labels - { - get - { - if (FunctionCallStack.Count == 0) - { - return topLabels; - } - else - { - return FunctionCallStack.Peek().Labels; - } - } - } - - public RuntimeInformation ParentRuntimeInformation - { - get => parentRuntimeInformation; - set - { - parentRuntimeInformation = value; - if (parentRuntimeInformation is not null) - { - parentRuntimeInformation.OnExit += ParentRuntimeInformation_OnExit; - } - } - } - - public bool IsSearching => !string.IsNullOrWhiteSpace(SearchLabel + SearchFunction) || IsInFunction && FunctionCallStack.Count == 0; - public bool IsLocalSearch { get; set; } - - private event Action OnExit; - - public event Action OnDebugOutput; - - public event Action OnLineExecuted; - - private void ParentRuntimeInformation_OnExit(string exitMessage, bool stopAllTasks) - { - Exit($"Terminated by parent task", stopAllTasks); - } - - public void WriteLine(string output, bool forceWrite = false) - { - if (Stop && !forceWrite || parentRuntimeInformation?.StopAllTasks == true && !forceWrite) - { - return; - } - - if (IsDebugMode) - { - if (IsTask) - { - parentRuntimeInformation!.WriteLine(output.FromSaveString(), forceWrite); - } - else - { - OnDebugOutput?.Invoke(output.FromSaveString() + Environment.NewLine); - } - } - else - { - Console.WriteLine(output.FromSaveString()); - } - } - - public void Write(string output, bool forceWrite = false) - { - if (Stop && !forceWrite || parentRuntimeInformation?.StopAllTasks == true && !forceWrite) - { - return; - } - - if (IsDebugMode) - { - if (IsTask) - { - parentRuntimeInformation!.Write(output.FromSaveString(), forceWrite); - } - else - { - OnDebugOutput?.Invoke(output.FromSaveString()); - } - } - else - { - Console.Write(output.FromSaveString()); - } - } - - public void Exit(string message, bool stopAllTasks) - { - if (!Stop) - { - 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; - Line stackLine = (ParentRuntimeInformation?.Lines ?? Lines).ElementAt(stackLineNumber); - WriteLine($" at line {stackLine.LineNumber + 1} in the file \"{stackLine.FileName}\"", true); - } - - Stop = true; - } - if (stopAllTasks && !StopAllTasks) - { - StopAllTasks = true; - OnExit?.Invoke(message, StopAllTasks); - parentRuntimeInformation?.Exit("Terminated by child task", true); - } - } - - public void LineExecuted(DebugEventArgs debugEventArgs) + if (IsDebugMode) { if (IsTask) { - parentRuntimeInformation.LineExecuted(debugEventArgs); + ParentRuntimeInformation!.WriteLine(output.FromSafeString(), forceWrite); } else { - OnLineExecuted?.Invoke(debugEventArgs); + OnDebugOutput?.Invoke(output.FromSafeString() + Environment.NewLine); } } - - public void Reset() + else { - topVariables.Clear(); - Lines.Clear(); - GloablVariables.Clear(); - Labels.Clear(); - Functions.Clear(); - FunctionCallStack.Clear(); - ParentRuntimeInformation = null; - SearchLabel = string.Empty; - SearchFunction = string.Empty; - CurrentFilePath = string.Empty; - CurrentLine = string.Empty; - Stop = false; - StopAllTasks = false; - IsDebugMode = false; - IsInFunction = false; - IsLocalSearch = false; - LineNumber = 0; - taskId = internalTaskId + 1; -#pragma warning disable S2696 // Instance members should not write to "static" fields - internalTaskId++; -#pragma warning restore S2696 // Instance members should not write to "static" fields + 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); + } } \ No newline at end of file diff --git a/YesNt.Interpreter/Runtime/StatementHandler.cs b/YesNt.Interpreter/Runtime/StatementHandler.cs new file mode 100644 index 0000000..1a2d66c --- /dev/null +++ b/YesNt.Interpreter/Runtime/StatementHandler.cs @@ -0,0 +1,10 @@ +using System; + +using YesNt.Interpreter.Attributes; + +namespace YesNt.Interpreter.Runtime; + +/// +/// Pre-calculated statement handler information for faster matching. +/// +internal record StatementHandler(StatementAttribute Attribute, Action Handler, string FullName); \ No newline at end of file diff --git a/YesNt.Interpreter/Runtime/StatementInformation.cs b/YesNt.Interpreter/Runtime/StatementInformation.cs index ed9489c..5e522b1 100644 --- a/YesNt.Interpreter/Runtime/StatementInformation.cs +++ b/YesNt.Interpreter/Runtime/StatementInformation.cs @@ -2,15 +2,34 @@ using YesNt.Interpreter.Enums; -namespace YesNt.Interpreter.Runtime +namespace YesNt.Interpreter.Runtime; + +/// +/// A read-only snapshot of a registered statement's metadata, used for tooling such as +/// syntax highlighters. Instances are obtained from . +/// +public class StatementInformation { - public class StatementInformation - { - public string Name { get; internal set; } - public SearchMode SearchMode { get; internal set; } - public SpaceAround SpaceAround { get; internal set; } - public ConsoleColor Color { get; internal set; } - public bool IgnoreSyntaxHighlighting { get; internal set; } - public string Seperator { get; set; } - } + /// Gets the keyword that identifies this statement in source code. + public string Name { get; internal set; } + + /// Gets where in the line the keyword is searched for. + public SearchMode SearchMode { get; internal set; } + + /// Gets which sides of the keyword must be padded with a space. + public SpaceAround SpaceAround { get; internal set; } + + /// Gets the syntax-highlight color for this statement. + public ConsoleColor Color { get; internal set; } + + /// + /// Gets a value indicating whether this statement is excluded from syntax highlighting. + /// + public bool IgnoreSyntaxHighlighting { get; internal set; } + + /// + /// Gets the optional sub-string that must be present in the line for this statement to match, + /// or if no separator is required. + /// + public string Separator { get; set; } } \ No newline at end of file diff --git a/YesNt.Interpreter/Runtime/StatementRuntimeInfo.cs b/YesNt.Interpreter/Runtime/StatementRuntimeInfo.cs index a0aaf2f..706456c 100644 --- a/YesNt.Interpreter/Runtime/StatementRuntimeInfo.cs +++ b/YesNt.Interpreter/Runtime/StatementRuntimeInfo.cs @@ -1,7 +1,24 @@ -namespace YesNt.Interpreter.Runtime +namespace YesNt.Interpreter.Runtime; + +/// +/// Base class for all classes that host statement handler methods. +/// Subclasses declare methods decorated with or +/// ; the source generator +/// (GeneratedStatementRegistry) discovers these at compile time and wires them up. +/// +internal abstract class StatementRuntimeInformation { - internal abstract class StatementRuntimeInformation + /// + /// Gets or sets the runtime state for the current execution context. + /// Injected by the generated registry before any handler is invoked. + /// + public RuntimeInformation RuntimeInfo { get; set; } + + /// + /// Trims surrounding whitespace and a trailing colon from a block or function name. + /// + protected static string NormalizeBlockName(string value) { - public RuntimeInformation RuntimeInfo { get; set; } + return value.Trim().TrimEnd(':').Trim(); } } \ No newline at end of file diff --git a/YesNt.Interpreter/Runtime/YesNtInterpreter.cs b/YesNt.Interpreter/Runtime/YesNtInterpreter.cs index 1b89015..2707aad 100644 --- a/YesNt.Interpreter/Runtime/YesNtInterpreter.cs +++ b/YesNt.Interpreter/Runtime/YesNtInterpreter.cs @@ -1,280 +1,549 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Linq; -using System.Reflection; using YesNt.Interpreter.Attributes; using YesNt.Interpreter.Enums; using YesNt.Interpreter.Utilities; -namespace YesNt.Interpreter.Runtime +namespace YesNt.Interpreter.Runtime; + +/// +/// The main entry point for executing YesNt scripts. +/// +public class YesNtInterpreter { - 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 { - private readonly RuntimeInformation runtimeInfo = new RuntimeInformation(); - private Dictionary> statements = new(); - private List> staticStatements = new(); - - public ReadOnlyCollection StatementInformation + get { - get + List information = statements.Select(s => { - List informations = statements.Select(s => + return new StatementInformation() { - return new StatementInformation() - { - Name = s.Key.Name, - SearchMode = s.Key.SearchMode, - SpaceAround = s.Key.SpaceAround, - Color = s.Key.Color, - IgnoreSyntaxHighlighting = s.Key.IgnoreSyntaxHighlighting, - Seperator = s.Key.Seperator - }; - }).ToList(); + 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(informations); + 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; } - } - public event Action OnLineExecuted; + Line lineObj = runtimeInfo.Lines[runtimeInfo.LineNumber]; + runtimeInfo.CurrentLine = lineObj.Content; - public event Action OnDebugOutput; - - public void Stop() - { - runtimeInfo.Exit("Terminated by external process", true); - } - - public void Initialize() - { - Assembly assembly = Assembly.GetExecutingAssembly(); - Type[] types = assembly.GetTypes(); - - IEnumerable statementRuntimeInfos = types.Where(t => t.IsSubclassOf(typeof(StatementRuntimeInformation))); - - statements.Clear(); - - foreach (Type type in statementRuntimeInfos) + if (string.IsNullOrWhiteSpace(runtimeInfo.CurrentLine) || runtimeInfo.CurrentLine.StartsWith('#')) { - object statementInfo = Activator.CreateInstance(type); + continue; + } - MethodInfo[] methodInfos = statementInfo.GetType().GetMethods(); - - StatementRuntimeInformation statementRuntimeInfo = statementInfo as StatementRuntimeInformation; - statementRuntimeInfo.RuntimeInfo = runtimeInfo; - - foreach (MethodInfo methodInfo in methodInfos) + DebugEventArgs debugEventArgs = null; + if (runtimeInfo.IsDebugMode) + { + debugEventArgs = new DebugEventArgs() { - StatementAttribute statementAttribute = methodInfo.GetCustomAttribute(); - if (statementAttribute is not null) - { - Action method = methodInfo.CreateDelegate(typeof(Action), statementInfo) as Action; - statements.Add(statementAttribute, method); - } + LineNumber = runtimeInfo.LineNumber + 1, + OriginalLine = runtimeInfo.CurrentLine.FromSafeString(), + IsTask = runtimeInfo.IsTask, + TaskId = runtimeInfo.TaskId + }; + } - StaticStatementAttribute staticStatementAttribute = methodInfo.GetCustomAttribute(); - if (staticStatementAttribute is not null) - { - Action method = methodInfo.CreateDelegate(typeof(Action), statementInfo) as Action; - staticStatements.Add(new(staticStatementAttribute, method)); - } + foreach (KeyValuePair staticStatement in staticStatements) + { + StaticStatementAttribute staticStatementAttribute = staticStatement.Key; + if (!staticStatementAttribute.ExecuteInSearchMode && runtimeInfo.IsSearching) + { + continue; } + + staticStatement.Value.Invoke(); } - statements = statements.OrderBy(s => s.Key.Priority).ToDictionary(x => x.Key, x => x.Value); - staticStatements = staticStatements.OrderBy(s => s.Key.Priority).ToList(); + bool statementFound = false; + bool notSearchingLabel = !runtimeInfo.IsSearching; - runtimeInfo.OnDebugOutput += (s) => OnDebugOutput?.Invoke(s); - runtimeInfo.OnLineExecuted += (DebugEventArgs e) => OnLineExecuted?.Invoke(e); - } + List handlers = (runtimeInfo.LineNumber < lineMatchingHandlers.Count) ? lineMatchingHandlers[runtimeInfo.LineNumber] : []; - public void Execute(string path, bool isDebugMode = false) - { - runtimeInfo.Reset(); - runtimeInfo.IsDebugMode = isDebugMode; - LoadFile(path); - Execute(); - } - - public void Execute(List lines, bool isDebugMode = false) - { - runtimeInfo.Reset(); - runtimeInfo.IsDebugMode = isDebugMode; - - for (int i = 0; i < lines.Count; i++) + foreach (StatementHandler handler in handlers) { - runtimeInfo.Lines.Add(new Line(lines[i], Path.GetFileName("#Memory#"), i)); - } + StatementAttribute statementAttribute = handler.Attribute; - Execute(); - } + if (!statementAttribute.ExecuteInSearchMode && runtimeInfo.IsSearching) + { + statementFound = true; + continue; + } - internal void Execute(List lines, Dictionary gloablVariables, int startLine, RuntimeInformation parentRuntimeInformation) - { - runtimeInfo.Reset(); - runtimeInfo.IsDebugMode = parentRuntimeInformation.IsDebugMode; - runtimeInfo.Lines = lines; - runtimeInfo.LineNumber = startLine; - runtimeInfo.ParentRuntimeInformation = parentRuntimeInformation; - runtimeInfo.GloablVariables = gloablVariables; - if (parentRuntimeInformation.StopAllTasks) - { - runtimeInfo.Exit($"Parent task was terminated!", 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.TrimEnd().Replace("\r", string.Empty); + string name = handler.FullName; - if (string.IsNullOrWhiteSpace(runtimeInfo.CurrentLine) || runtimeInfo.CurrentLine.StartsWith('#')) + if (statementAttribute.Separator is null || runtimeInfo.CurrentLine.Contains(statementAttribute.Separator, StringComparison.Ordinal)) { - continue; - } - - DebugEventArgs debugEventArgs = new DebugEventArgs() - { - LineNumber = runtimeInfo.LineNumber + 1, - OriginalLine = runtimeInfo.CurrentLine.FromSaveString(), - 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) + 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; - continue; } - - if (runtimeInfo.Stop) + else if (statementAttribute.SearchMode == SearchMode.Contains && runtimeInfo.CurrentLine.Contains(name, StringComparison.Ordinal)) { - break; + string copyLine = statementAttribute.KeepStatementInArgs ? runtimeInfo.CurrentLine : runtimeInfo.CurrentLine.Replace(name, string.Empty); + handler.Handler.Invoke(copyLine); + statementFound = true; } - - string name = statementAttribute.SpaceAround switch + else if (statementAttribute.SearchMode == SearchMode.EndOfLine && runtimeInfo.CurrentLine.EndsWith(name, StringComparison.Ordinal)) { - SpaceAround.StartEnd => $" {statementAttribute.Name.Trim()} ", - SpaceAround.Start => $" {statementAttribute.Name.Trim()}", - SpaceAround.End => $"{statementAttribute.Name.Trim()} ", - _ => statementAttribute.Name - }; - - if (statementAttribute.Seperator is null || runtimeInfo.CurrentLine.Contains(statementAttribute.Seperator)) + 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)) { - if (statementAttribute.SearchMode == SearchMode.StartOfLine && runtimeInfo.CurrentLine.StartsWith(name)) - { - string copyLine = statementAttribute.KeepStatementInArgs ? runtimeInfo.CurrentLine : runtimeInfo.CurrentLine.Remove(0, name.Length); - statement.Value.Invoke(copyLine); - statementFound = true; - } - else if (statementAttribute.SearchMode == SearchMode.Contains && $" {runtimeInfo.CurrentLine} ".Contains(name)) - { - bool leadingWhitespace = runtimeInfo.CurrentLine.StartsWith(' '); - - runtimeInfo.CurrentLine = $" {runtimeInfo.CurrentLine} "; - string copyLine = statementAttribute.KeepStatementInArgs ? runtimeInfo.CurrentLine : runtimeInfo.CurrentLine.Replace(name, string.Empty); - statement.Value.Invoke(copyLine); - statementFound = true; - - if (!leadingWhitespace) - { - runtimeInfo.CurrentLine = runtimeInfo.CurrentLine.Trim(); - } - else - { - runtimeInfo.CurrentLine = runtimeInfo.CurrentLine.TrimEnd(); - } - } - else if (statementAttribute.SearchMode == SearchMode.EndOfLine && runtimeInfo.CurrentLine.EndsWith(name)) - { - string copyLine = statementAttribute.KeepStatementInArgs ? runtimeInfo.CurrentLine : runtimeInfo.CurrentLine.Remove(runtimeInfo.CurrentLine.Length - 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; - } + handler.Handler.Invoke(runtimeInfo.CurrentLine); + statementFound = true; } - } - - if (!statementFound) - { - runtimeInfo.Exit("Invalid statement", true); - } - if (runtimeInfo.IsDebugMode && notSearchingLabel) - { - debugEventArgs.CurrentLine = runtimeInfo.CurrentLine.FromSaveString(); - runtimeInfo.LineExecuted(debugEventArgs); } } - if (!runtimeInfo.Stop) + if (!statementFound) { - if (!string.IsNullOrWhiteSpace(runtimeInfo.SearchLabel)) - { - runtimeInfo.Exit($"Label \"{runtimeInfo.SearchLabel}\" not found", true); - } - else if (!string.IsNullOrWhiteSpace(runtimeInfo.SearchFunction)) - { - runtimeInfo.Exit($"Function \"{runtimeInfo.SearchFunction}\" not found", true); - } - else - { - runtimeInfo.Exit("End of file", false); - } - - if (runtimeInfo.IsDebugMode) - { - runtimeInfo.LineExecuted(null); - } + runtimeInfo.Exit(ExitMessages.InvalidStatement, true); + } + if (runtimeInfo.IsDebugMode && notSearchingLabel && debugEventArgs != null) + { + debugEventArgs.CurrentLine = runtimeInfo.CurrentLine.FromSafeString(); + runtimeInfo.LineExecuted(debugEventArgs); } } - private void LoadFile(string path) + if (!runtimeInfo.Stop) { - if (!File.Exists(path)) + if (!string.IsNullOrWhiteSpace(runtimeInfo.SearchLabel)) { - runtimeInfo.Exit($"File \"{path}\" not found!", true); - return; + 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); } - string[] lines = File.ReadAllLines(path); - - for (int i = 0; i < lines.Length; i++) + if (runtimeInfo.IsDebugMode) { - runtimeInfo.Lines.Add(new Line(lines[i], Path.GetFileName(path), i)); + 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 + }; + } } \ No newline at end of file diff --git a/YesNt.Interpreter/Statements/CodeFlowStatements.cs b/YesNt.Interpreter/Statements/CodeFlowStatements.cs index 61bafcf..ff6536d 100644 --- a/YesNt.Interpreter/Statements/CodeFlowStatements.cs +++ b/YesNt.Interpreter/Statements/CodeFlowStatements.cs @@ -1,198 +1,283 @@ -using System; +using System; using System.Collections.Generic; -using System.Linq; using YesNt.Interpreter.Attributes; using YesNt.Interpreter.Enums; using YesNt.Interpreter.Runtime; using YesNt.Interpreter.Utilities; -namespace YesNt.Interpreter.Statements +namespace YesNt.Interpreter.Statements; + +internal class CodeFlowStatements : StatementRuntimeInformation { - internal class CodeFlowStatements : StatementRuntimeInformation + [Statement("goto", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow)] + public void Jump(string args) { - [Statement("jmp", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow)] - public void Jump(string args) - { - string key = args.Trim(); + string key = NormalizeBlockName(args); - if (RuntimeInfo.Labels.ContainsKey(key)) - { - RuntimeInfo.LineNumber = RuntimeInfo.Labels[key]; - } - else - { - RuntimeInfo.SearchLabel = key; - RuntimeInfo.IsLocalSearch = RuntimeInfo.IsInFunction; - } + if (RuntimeInfo.Labels.TryGetValue(key, out int value)) + { + RuntimeInfo.LineNumber = value; } - - [Statement("jif", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow, Seperator = "|")] - public void JumpIf(string args) + else { - string[] parts = args.Split('|'); - if (parts.Length != 2) - { - RuntimeInfo.Exit("Invalid syntax", true); - return; - } - - string key = parts[0].Trim(); - string condition = parts[1].Trim(); - - bool? result = Evaluator.EvaluateCondition(condition); - - if (result is null) - { - RuntimeInfo.Exit("Invalid operation", true); - return; - } - - if (result == false) - { - return; - } - - if (RuntimeInfo.Labels.ContainsKey(key)) - { - RuntimeInfo.LineNumber = RuntimeInfo.Labels[key]; - } - else - { - RuntimeInfo.SearchLabel = key; - RuntimeInfo.IsLocalSearch = RuntimeInfo.IsInFunction; - } - } - - [Statement("lbl", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, ExecuteInSearchMode = true)] - public void FindLabel(string args) - { - string key = args.Trim(); - if (RuntimeInfo.Labels.ContainsKey(key)) - { - RuntimeInfo.Labels[key] = RuntimeInfo.LineNumber; - } - else - { - RuntimeInfo.Labels.Add(key, RuntimeInfo.LineNumber); - } - - if (!string.IsNullOrWhiteSpace(RuntimeInfo.SearchLabel) && RuntimeInfo.SearchLabel == key) - { - RuntimeInfo.SearchLabel = string.Empty; - RuntimeInfo.IsLocalSearch = false; - } - } - - [Statement("cal", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, Priority = Priority.VeryLow)] - public void Call(string args) - { - string key = args.Trim(); - - RuntimeInfo.FunctionCallStack.Push(new FunctionScope(RuntimeInfo.LineNumber, new Stack(RuntimeInfo.InParametersStack.Reverse()))); - RuntimeInfo.InParametersStack.Clear(); - - if (RuntimeInfo.Functions.ContainsKey(key)) - { - RuntimeInfo.LineNumber = RuntimeInfo.Functions[key]; - } - else - { - RuntimeInfo.SearchFunction = key; - } - } - - [Statement("cif", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, Priority = Priority.VeryLow, Seperator = "|")] - public void CallIf(string args) - { - string[] parts = args.Split('|'); - if (parts.Length != 2) - { - RuntimeInfo.Exit("Invalid syntax", true); - return; - } - - string key = parts[0].Trim(); - string condition = parts[1].Trim(); - - bool? result = Evaluator.EvaluateCondition(condition); - - if (result is null) - { - RuntimeInfo.Exit("Invalid operation", true); - return; - } - - if (result == false) - { - return; - } - - RuntimeInfo.FunctionCallStack.Push(new FunctionScope(RuntimeInfo.LineNumber, new Stack(RuntimeInfo.InParametersStack))); - RuntimeInfo.InParametersStack.Clear(); - - if (RuntimeInfo.Functions.ContainsKey(key)) - { - RuntimeInfo.LineNumber = RuntimeInfo.Functions[key]; - } - else - { - RuntimeInfo.SearchFunction = key; - } - } - - [Statement("end", SearchMode.Exact, SpaceAround.None, ConsoleColor.Red, ExecuteInSearchMode = true)] - public void End(string _) - { - if (RuntimeInfo.IsSearching) - { - RuntimeInfo.IsInFunction = false; - if (RuntimeInfo.IsLocalSearch) - { - RuntimeInfo.Exit($"Label \"{RuntimeInfo.SearchLabel}\" not found", true); - } - - return; - } - else - { - RuntimeInfo.IsInFunction = false; - } - - RuntimeInfo.Exit("Planned termination by code", false); - } - - [Statement("trm", SearchMode.Exact, SpaceAround.None, ConsoleColor.Red, ExecuteInSearchMode = true)] - public void Terminate(string _) - { - if (RuntimeInfo.IsSearching) - { - RuntimeInfo.IsInFunction = false; - if (RuntimeInfo.IsLocalSearch) - { - RuntimeInfo.Exit($"Label \"{RuntimeInfo.SearchLabel}\" not found", true); - } - - return; - } - else - { - RuntimeInfo.IsInFunction = false; - } - - RuntimeInfo.Exit("Planned termination by code. Canceling all tasks", true); - } - - [Statement("trw", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Red)] - public void Throw(string message) - { - RuntimeInfo.Exit(message, true); - } - - [Statement("err", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Red)] - public void Error(string message) - { - RuntimeInfo.Exit(message, false); + RuntimeInfo.SearchLabel = key; + RuntimeInfo.IsLocalSearch = RuntimeInfo.IsInFunction; } } + + [Statement("if", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow, Separator = " goto ")] + public void JumpIf(string args) + { + string[] parts = args.Split(" goto ", 2, StringSplitOptions.None); + if (parts.Length != 2) + { + RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true); + return; + } + + string condition = parts[0].Trim(); + string key = NormalizeBlockName(parts[1]); + + bool? result = Evaluator.EvaluateCondition(condition); + + if (result is null) + { + RuntimeInfo.Exit(ExitMessages.InvalidOperation, true); + return; + } + + if (result == false) + { + return; + } + + if (RuntimeInfo.Labels.TryGetValue(key, out int value)) + { + RuntimeInfo.LineNumber = value; + } + else + { + RuntimeInfo.SearchLabel = key; + RuntimeInfo.IsLocalSearch = RuntimeInfo.IsInFunction; + } + } + + [Statement("label", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, ExecuteInSearchMode = true, Separator = ":")] + public void FindLabel(string args) + { + string labelDeclaration = args.Trim(); + if (!labelDeclaration.EndsWith(':')) + { + RuntimeInfo.Exit(ExitMessages.InvalidSyntaxColonRequired, true); + return; + } + + string key = NormalizeBlockName(labelDeclaration); + if (string.IsNullOrWhiteSpace(key)) + { + RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true); + return; + } + + RuntimeInfo.Labels[key] = RuntimeInfo.LineNumber; + + if (!string.IsNullOrWhiteSpace(RuntimeInfo.SearchLabel) && RuntimeInfo.SearchLabel == key) + { + RuntimeInfo.SearchLabel = string.Empty; + RuntimeInfo.IsLocalSearch = false; + } + } + + [Statement("call", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, Priority = Priority.VeryLow)] + public void Call(string args) + { + CallFunction(NormalizeBlockName(args)); + } + + [Statement("if", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, Priority = Priority.VeryLow, Separator = " call ")] + public void CallIf(string args) + { + string[] parts = args.Split(" call ", 2, StringSplitOptions.None); + if (parts.Length != 2) + { + RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true); + return; + } + + string condition = parts[0].Trim(); + bool? result = Evaluator.EvaluateCondition(condition); + + if (result is null) + { + RuntimeInfo.Exit(ExitMessages.InvalidOperation, true); + return; + } + + if (result == true) + { + CallFunction(NormalizeBlockName(parts[1])); + } + } + + [Statement("if", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow, Separator = ":", BlockPair = "end_if")] + public void IfBlock(string args) + { + args = args.Trim(); + if (!args.EndsWith(':')) + { + RuntimeInfo.Exit(ExitMessages.InvalidSyntaxColonRequired, true); + return; + } + + string condition = args[..^1].Trim(); + bool? result = Evaluator.EvaluateCondition(condition); + + if (result is null) + { + RuntimeInfo.Exit(ExitMessages.InvalidOperation, true); + return; + } + + if (result == true) + { + return; + } + + int targetLine = FindBlockBoundary(RuntimeInfo.LineNumber); + if (targetLine < 0) + { + RuntimeInfo.Exit(ExitMessages.NoMatchingEndIf, true); + return; + } + + RuntimeInfo.LineNumber = targetLine; + } + + [Statement("else:", SearchMode.Exact, SpaceAround.None, ConsoleColor.Green, IsBlockIntermediate = true, BlockPair = "end_if")] + public void Else(string _) + { + int targetLine = FindBlockBoundary(RuntimeInfo.LineNumber); + if (targetLine < 0) + { + RuntimeInfo.Exit(ExitMessages.NoMatchingEndIf, true); + return; + } + + RuntimeInfo.LineNumber = targetLine; + } + + [Statement("end_if", SearchMode.Exact, SpaceAround.None, ConsoleColor.Green, IsBlockEnd = true)] + public void EndIf(string _) + { + // Intentionally empty: end_if is a block-boundary marker only; no runtime action needed. + } + + [Statement("while", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow, Separator = ":", BlockPair = "end_while")] + public void While(string args) + { + args = args.Trim(); + if (!args.EndsWith(':')) + { + RuntimeInfo.Exit(ExitMessages.InvalidSyntaxColonRequired, true); + return; + } + + string condition = args[..^1].Trim(); + bool? result = Evaluator.EvaluateCondition(condition); + + if (result is null) + { + RuntimeInfo.Exit(ExitMessages.InvalidOperation, true); + return; + } + + if (result == true) + { + return; + } + + int endWhileLine = FindBlockBoundary(RuntimeInfo.LineNumber); + if (endWhileLine < 0) + { + RuntimeInfo.Exit(ExitMessages.NoMatchingEndWhile, true); + return; + } + + RuntimeInfo.LineNumber = endWhileLine; + } + + [Statement("end_while", SearchMode.Exact, SpaceAround.None, ConsoleColor.Green, IsBlockEnd = true)] + public void EndWhile(string _) + { + int whileLine = FindBlockBoundary(RuntimeInfo.LineNumber); + if (whileLine < 0) + { + RuntimeInfo.Exit(ExitMessages.NoMatchingWhile, true); + return; + } + + RuntimeInfo.LineNumber = whileLine - 1; + } + + [Statement("exit", SearchMode.Exact, SpaceAround.None, ConsoleColor.Red, ExecuteInSearchMode = true)] + public void End(string _) + { + HandleExit(ExitMessages.PlannedTermination, false); + } + + [Statement("abort_all", SearchMode.Exact, SpaceAround.None, ConsoleColor.Red, ExecuteInSearchMode = true)] + public void Terminate(string _) + { + HandleExit(ExitMessages.PlannedTerminationCancelingTasks, true); + } + + [Statement("throw", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Red)] + public void Throw(string message) + { + RuntimeInfo.Exit(message, true); + } + + [Statement("error", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Red)] + public void Error(string message) + { + RuntimeInfo.Exit(message, false); + } + + private void CallFunction(string key) + { + RuntimeInfo.FunctionCallStack.Push(new FunctionScope(RuntimeInfo.LineNumber, new Stack(RuntimeInfo.InParametersStack))); + RuntimeInfo.InParametersStack.Clear(); + + if (RuntimeInfo.Functions.TryGetValue(key, out int value)) + { + RuntimeInfo.LineNumber = value; + } + else + { + RuntimeInfo.SearchFunction = key; + } + } + + private void HandleExit(string exitMessage, bool isError) + { + if (RuntimeInfo.IsSearching) + { + RuntimeInfo.IsInFunction = false; + if (RuntimeInfo.IsLocalSearch) + { + RuntimeInfo.Exit(ExitMessages.LabelNotFound(RuntimeInfo.SearchLabel), true); + } + + return; + } + + RuntimeInfo.IsInFunction = false; + RuntimeInfo.Exit(exitMessage, isError); + } + + private int FindBlockBoundary(int currentLine) + { + return RuntimeInfo.BlockBoundaries.TryGetValue(currentLine, out int cached) ? cached : -1; + } } \ No newline at end of file diff --git a/YesNt.Interpreter/Statements/ConsoleStatements.cs b/YesNt.Interpreter/Statements/ConsoleStatements.cs index 1d07072..7df8c71 100644 --- a/YesNt.Interpreter/Statements/ConsoleStatements.cs +++ b/YesNt.Interpreter/Statements/ConsoleStatements.cs @@ -6,56 +6,61 @@ using YesNt.Interpreter.Enums; using YesNt.Interpreter.Runtime; using YesNt.Interpreter.Utilities; -namespace YesNt.Interpreter.Statements +namespace YesNt.Interpreter.Statements; + +internal class ConsoleStatements : StatementRuntimeInformation { - internal class ConsoleStatements : StatementRuntimeInformation + [Statement("print_line", SearchMode.Exact, SpaceAround.None, ConsoleColor.DarkGreen, Priority = Priority.VeryLow)] + public void WriteLineEmpty(string _) { - [Statement("cwl", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkGreen, Priority = Priority.VeryLow)] - public void WriteLine(string args) - { - RuntimeInfo.WriteLine(args); - } + RuntimeInfo.WriteLine(string.Empty); + } - [Statement("cw", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkGreen, Priority = Priority.VeryLow)] - public void Write(string args) - { - RuntimeInfo.Write(args); - } + [Statement("print_line", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkGreen, Priority = Priority.VeryLow)] + public void WriteLine(string args) + { + RuntimeInfo.WriteLine(args); + } - [Statement("%crl", SearchMode.Contains, SpaceAround.End, ConsoleColor.DarkGreen, KeepStatementInArgs = true, Priority = Priority.Highest)] - public void ReadLine(string args) + [Statement("print", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkGreen, Priority = Priority.VeryLow)] + public void Write(string args) + { + RuntimeInfo.Write(args); + } + + [Statement("%read_line", SearchMode.Contains, SpaceAround.None, ConsoleColor.DarkGreen, KeepStatementInArgs = true, Priority = Priority.Highest)] + public void ReadLine(string args) + { + args += " "; + while (args.Contains("%read_line")) { - args += " "; - while (args.Contains("%crl ")) + string input = Console.ReadLine(); + if (input is null) { - string input = Console.ReadLine(); - if (input is null) - { - RuntimeInfo.Exit("Terminated by external process", true); - return; - } - args = args.ReplaceFirstOccurrence("%crl ", input.ToSaveString() + " "); + RuntimeInfo.Exit(ExitMessages.TerminatedByExternalProcess, true); + return; } - RuntimeInfo.CurrentLine = args.TrimEnd(); + args = args.ReplaceFirstOccurrence("%read_line ", input.ToSafeString() + " "); } + RuntimeInfo.CurrentLine = args.TrimEnd(); + } - [Statement("%cr", SearchMode.Contains, SpaceAround.End, ConsoleColor.DarkGreen, KeepStatementInArgs = true, Priority = Priority.Highest)] - public void ReadKey(string args) + [Statement("%read_key", SearchMode.Contains, SpaceAround.None, ConsoleColor.DarkGreen, KeepStatementInArgs = true, Priority = Priority.Highest)] + public void ReadKey(string args) + { + args += " "; + while (args.Contains("%read_key")) { - args += " "; - while (args.Contains("%cr ")) - { - string input = ConsoleExtentions.ReadKey(RuntimeInfo).ToString(); - args = args.ReplaceFirstOccurrence("%cr ", input.ToSaveString() + " "); - } - RuntimeInfo.CurrentLine = args.TrimEnd(); + string input = ConsoleExtensions.ReadKey(RuntimeInfo).ToString(); + args = args.ReplaceFirstOccurrence("%read_key ", input.ToSafeString() + " "); } + RuntimeInfo.CurrentLine = args.TrimEnd(); + } - [Statement("cls", SearchMode.Exact, SpaceAround.None, ConsoleColor.Magenta)] - [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Won't work if static")] - public void Clear(string _) - { - Console.Clear(); - } + [Statement("clear", SearchMode.Exact, SpaceAround.None, ConsoleColor.Magenta)] + [SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Won't work if static")] + public void Clear(string _) + { + Console.Clear(); } } \ No newline at end of file diff --git a/YesNt.Interpreter/Statements/FunctionStatements.cs b/YesNt.Interpreter/Statements/FunctionStatements.cs index 23d67a6..d6398fb 100644 --- a/YesNt.Interpreter/Statements/FunctionStatements.cs +++ b/YesNt.Interpreter/Statements/FunctionStatements.cs @@ -1,200 +1,176 @@ -using System; +using System; using System.Collections.Generic; using YesNt.Interpreter.Attributes; using YesNt.Interpreter.Enums; using YesNt.Interpreter.Runtime; +using YesNt.Interpreter.Utilities; -namespace YesNt.Interpreter.Statements +namespace YesNt.Interpreter.Statements; + +internal class FunctionStatements : StatementRuntimeInformation { - internal class FunctionStatements : StatementRuntimeInformation + [Statement("func", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, ExecuteInSearchMode = true, Separator = ":")] + public void FindFunction(string args) { - [Statement("fnc", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, ExecuteInSearchMode = true)] - public void FindFunction(string args) + if (RuntimeInfo.InternalIsInFunction) { - if (RuntimeInfo.InternalIsInFunction) - { - RuntimeInfo.Exit("Nested functions are not allowed", true); - return; - } - - string key = args.Trim(); - if (RuntimeInfo.Functions.ContainsKey(key)) - { - RuntimeInfo.Functions[key] = RuntimeInfo.LineNumber; - } - else - { - RuntimeInfo.Functions.Add(key, RuntimeInfo.LineNumber); - } - - if (!string.IsNullOrWhiteSpace(RuntimeInfo.SearchFunction) && RuntimeInfo.SearchFunction == key) - { - RuntimeInfo.SearchFunction = string.Empty; - } - - RuntimeInfo.IsInFunction = true; + RuntimeInfo.Exit(ExitMessages.NestedFunctionsNotAllowed, true); + return; } - [Statement("in", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Yellow)] - public void AddInParameter(string args) + string functionDeclaration = args.Trim(); + if (!functionDeclaration.EndsWith(':')) { - RuntimeInfo.InParametersStack.Push(args); + RuntimeInfo.Exit(ExitMessages.InvalidSyntaxColonRequired, true); + return; } - [Statement("out", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Yellow)] - public void GetOutParameter(string args) + string key = NormalizeBlockName(functionDeclaration); + if (string.IsNullOrWhiteSpace(key)) { - if (RuntimeInfo.OutParametersStack.Count == 0) - { - RuntimeInfo.Exit("No out argument in stack", true); - return; - } - - if (RuntimeInfo.Variables.ContainsKey(args)) - { - RuntimeInfo.Variables[args] = RuntimeInfo.OutParametersStack.Pop(); - } - else - { - RuntimeInfo.Variables.Add(args, RuntimeInfo.OutParametersStack.Pop()); - } + RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true); + return; } - [Statement("%iso", SearchMode.Contains, SpaceAround.None, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)] - public void CheckIfOutParameterAvalible(string args) - { - args += " "; - args = args.Replace("%iso", (RuntimeInfo.OutParametersStack.Count > 0).ToString()); + RuntimeInfo.Functions[key] = RuntimeInfo.LineNumber; - RuntimeInfo.CurrentLine = args.TrimEnd(); + if (!string.IsNullOrWhiteSpace(RuntimeInfo.SearchFunction) && RuntimeInfo.SearchFunction == key) + { + RuntimeInfo.SearchFunction = string.Empty; } - [Statement("cal", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, Priority = Priority.Low, Seperator = "|")] - public void Call(string args) + RuntimeInfo.IsInFunction = true; + } + + [Statement("push_in", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Yellow)] + public void AddInParameter(string args) + { + RuntimeInfo.InParametersStack.Push(args); + } + + [Statement("%out", SearchMode.Contains, SpaceAround.None, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)] + public void GetOutParameter(string args) + { + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessStackParameters(args, "%out", RuntimeInfo.OutParametersStack, RuntimeInfo, ExitMessages.NoOutArgumentInStack); + } + + [Statement("%has_out", SearchMode.Contains, SpaceAround.None, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)] + public void CheckIfOutParameterAvailable(string args) + { + args = args.Replace("%has_out", (RuntimeInfo.OutParametersStack.Count > 0).ToString()); + + RuntimeInfo.CurrentLine = args.TrimEnd(); + } + + [Statement("call", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, Priority = Priority.Low, Separator = " with ")] + public void Call(string args) + { + string[] parts = args.Split(" with ", 2, StringSplitOptions.None); + if (parts.Length != 2) { - string[] parts = args.Split('|'); - if (parts.Length != 2) - { - RuntimeInfo.Exit("Invalid syntax", true); - return; - } - - string key = parts[0].Trim(); - string[] functionArgumets = parts[1].Split(','); - - foreach (string argumanet in functionArgumets) - { - RuntimeInfo.InParametersStack.Push(argumanet.Trim()); - } - - RuntimeInfo.FunctionCallStack.Push(new FunctionScope(RuntimeInfo.LineNumber, new Stack(RuntimeInfo.InParametersStack))); - RuntimeInfo.InParametersStack.Clear(); - RuntimeInfo.CurrentLine = string.Empty; - - if (RuntimeInfo.Functions.ContainsKey(key)) - { - RuntimeInfo.LineNumber = RuntimeInfo.Functions[key]; - } - else - { - RuntimeInfo.SearchFunction = key; - } + RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true); + return; } - [Statement("get", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Yellow)] - public void GetInParameter(string args) + string key = NormalizeBlockName(parts[0]); + string[] functionArguments = parts[1].Split(','); + + foreach (string argument in functionArguments) { - if (!RuntimeInfo.IsInFunction) - { - RuntimeInfo.Exit("Statement not allowed outside of function", true); - return; - } - - if (RuntimeInfo.FunctionCallStack.Peek().Arguemtns.Count == 0) - { - RuntimeInfo.Exit("No in argument in stack", true); - return; - } - - if (RuntimeInfo.Variables.ContainsKey(args)) - { - RuntimeInfo.Variables[args] = RuntimeInfo.FunctionCallStack.Peek().Arguemtns.Pop(); - } - else - { - RuntimeInfo.Variables.Add(args, RuntimeInfo.FunctionCallStack.Peek().Arguemtns.Pop()); - } + RuntimeInfo.InParametersStack.Push(argument.Trim()); } - [Statement("%isi", SearchMode.Contains, SpaceAround.End, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)] - public void CheckIfInParameterAvalible(string args) + RuntimeInfo.FunctionCallStack.Push(new FunctionScope(RuntimeInfo.LineNumber, new Stack(RuntimeInfo.InParametersStack))); + RuntimeInfo.InParametersStack.Clear(); + RuntimeInfo.CurrentLine = string.Empty; + + if (RuntimeInfo.Functions.TryGetValue(key, out int value)) { - if (!RuntimeInfo.IsInFunction) - { - RuntimeInfo.Exit("Statement not allowed outside of function", true); - return; - } - - args += " "; - args = args.Replace("%isi", (RuntimeInfo.FunctionCallStack.Peek().Arguemtns.Count > 0).ToString()); - - RuntimeInfo.CurrentLine = args.TrimEnd(); + RuntimeInfo.LineNumber = value; } - - [Statement("put", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Yellow)] - public void AddOutParameter(string args) + else { - if (!RuntimeInfo.IsInFunction) - { - RuntimeInfo.Exit("Statement not allowed outside of function", true); - return; - } - - RuntimeInfo.FunctionCallStack.Peek().Results.Push(args); - } - - [Statement("ret", SearchMode.Exact, SpaceAround.None, ConsoleColor.DarkYellow, ExecuteInSearchMode = true)] - public void Return(string _) - { - if (!RuntimeInfo.IsInFunction) - { - RuntimeInfo.Exit("Statement not allowed outside of function", true); - return; - } - - if (RuntimeInfo.IsSearching) - { - RuntimeInfo.IsInFunction = false; - - if (RuntimeInfo.IsLocalSearch) - { - RuntimeInfo.Exit($"Label \"{RuntimeInfo.SearchLabel}\" not found", true); - } - return; - } - else - { - RuntimeInfo.IsInFunction = false; - } - - if (RuntimeInfo.FunctionCallStack.Count > 0) - { - FunctionScope functionScope = RuntimeInfo.FunctionCallStack.Pop(); - - RuntimeInfo.OutParametersStack = new Stack(functionScope.Results); - RuntimeInfo.LineNumber = functionScope.CallerLine; - } - else - { - RuntimeInfo.Exit("No function in stack", true); - } - } - - [Statement("ccs", SearchMode.Exact, SpaceAround.None, ConsoleColor.Red)] - public void ClearCallStack(string _) - { - RuntimeInfo.FunctionCallStack.Clear(); + RuntimeInfo.SearchFunction = key; } } + + [Statement("%in", SearchMode.Contains, SpaceAround.None, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)] + public void GetInParameter(string args) + { + if (!RuntimeInfo.IsInFunction) + { + RuntimeInfo.Exit(ExitMessages.StatementNotAllowedOutsideFunction, true); + return; + } + + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessStackParameters(args, "%in", RuntimeInfo.FunctionCallStack.Peek().Arguments, RuntimeInfo, ExitMessages.NoInArgumentInStack); + } + + [Statement("%has_in", SearchMode.Contains, SpaceAround.None, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)] + public void CheckIfInParameterAvailable(string args) + { + if (!RuntimeInfo.IsInFunction) + { + RuntimeInfo.Exit(ExitMessages.StatementNotAllowedOutsideFunction, true); + return; + } + + args = args.Replace("%has_in", (RuntimeInfo.FunctionCallStack.Peek().Arguments.Count > 0).ToString()); + + RuntimeInfo.CurrentLine = args.TrimEnd(); + } + + [Statement("push_out", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Yellow)] + public void AddOutParameter(string args) + { + if (!RuntimeInfo.IsInFunction) + { + RuntimeInfo.Exit(ExitMessages.StatementNotAllowedOutsideFunction, true); + return; + } + + RuntimeInfo.FunctionCallStack.Peek().Results.Push(args); + } + + [Statement("return", SearchMode.Exact, SpaceAround.None, ConsoleColor.DarkYellow, ExecuteInSearchMode = true)] + public void Return(string _) + { + if (!RuntimeInfo.IsInFunction) + { + RuntimeInfo.Exit(ExitMessages.StatementNotAllowedOutsideFunction, true); + return; + } + + if (RuntimeInfo.IsSearching) + { + RuntimeInfo.IsInFunction = false; + + if (RuntimeInfo.IsLocalSearch) + { + RuntimeInfo.Exit(ExitMessages.LabelNotFound(RuntimeInfo.SearchLabel), true); + } + return; + } + + RuntimeInfo.IsInFunction = false; + + if (RuntimeInfo.FunctionCallStack.Count > 0) + { + FunctionScope functionScope = RuntimeInfo.FunctionCallStack.Pop(); + + RuntimeInfo.OutParametersStack = new Stack(functionScope.Results); + RuntimeInfo.LineNumber = functionScope.CallerLine; + } + else + { + RuntimeInfo.Exit(ExitMessages.NoFunctionInStack, true); + } + } + + [Statement("clear_call_stack", SearchMode.Exact, SpaceAround.None, ConsoleColor.Red)] + public void ClearCallStack(string _) + { + RuntimeInfo.FunctionCallStack.Clear(); + } } \ No newline at end of file diff --git a/YesNt.Interpreter/Statements/ListStatements.cs b/YesNt.Interpreter/Statements/ListStatements.cs new file mode 100644 index 0000000..04c1d41 --- /dev/null +++ b/YesNt.Interpreter/Statements/ListStatements.cs @@ -0,0 +1,274 @@ +using System; +using System.Collections.Generic; + +using YesNt.Interpreter.Attributes; +using YesNt.Interpreter.Enums; +using YesNt.Interpreter.Runtime; + +namespace YesNt.Interpreter.Statements; + +internal class ListStatements : StatementRuntimeInformation +{ + [Statement("list", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " new")] + public void Create(string args) + { + string[] parts = SplitTwo(args, " new"); + if (parts.Length == 0) + { + return; + } + + string name = parts[0]; + if (!RuntimeInfo.Lists.TryGetValue(name, out List value)) + { + RuntimeInfo.Lists.Add(name, []); + } + else + { + value.Clear(); + } + } + + [Statement("list", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " delete")] + public void Delete(string args) + { + string[] parts = SplitTwo(args, " delete"); + if (parts.Length == 0) + { + return; + } + + string name = parts[0]; + + if (!RuntimeInfo.Lists.ContainsKey(name)) + { + RuntimeInfo.Exit(ExitMessages.ListNotFound(name), true); + return; + } + + _ = RuntimeInfo.Lists.Remove(name); + } + + [Statement("list", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " clear")] + public void Clear(string args) + { + string[] parts = SplitTwo(args, " clear"); + if (parts.Length == 0) + { + return; + } + + if (!TryGetList(parts[0], out List list)) + { + return; + } + + list.Clear(); + } + + [Statement("list", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " length")] + public void Length(string args) + { + string[] parts = SplitTwo(args, " length"); + if (parts.Length == 0) + { + return; + } + + if (!TryGetList(parts[0], out List list)) + { + return; + } + + RuntimeInfo.OutParametersStack.Clear(); + RuntimeInfo.OutParametersStack.Push(list.Count.ToString()); + } + + [Statement("list", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " add ")] + public void Add(string args) + { + string[] parts = SplitTwo(args, " add "); + if (parts.Length == 0) + { + return; + } + + if (!TryGetList(parts[0], out List list)) + { + return; + } + + if (string.IsNullOrWhiteSpace(parts[1])) + { + RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true); + return; + } + + list.Add(parts[1].Trim()); + } + + [Statement("list", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " get ")] + public void Get(string args) + { + string[] parts = SplitTwo(args, " get "); + if (parts.Length == 0) + { + return; + } + + if (!TryGetList(parts[0], out List list)) + { + return; + } + + if (!TryParseIndex(parts[1], out int index, list.Count)) + { + return; + } + + RuntimeInfo.OutParametersStack.Clear(); + RuntimeInfo.OutParametersStack.Push(list[index]); + } + + [Statement("list", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " remove ")] + public void Remove(string args) + { + string[] parts = SplitTwo(args, " remove "); + if (parts.Length == 0) + { + return; + } + + if (!TryGetList(parts[0], out List list)) + { + return; + } + + if (!TryParseIndex(parts[1], out int index, list.Count)) + { + return; + } + + list.RemoveAt(index); + } + + [Statement("list", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " set ")] + public void Set(string args) + { + string[] parts = SplitTwo(args, " set "); + if (parts.Length == 0) + { + return; + } + + if (!TryGetList(parts[0], out List list)) + { + return; + } + + string[] indexAndValue = SplitIndexAndValue(parts[1]); + if (indexAndValue.Length == 0) + { + return; + } + + if (!TryParseIndex(indexAndValue[0], out int index, list.Count)) + { + return; + } + + list[index] = indexAndValue[1]; + } + + [Statement("list", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " insert ")] + public void Insert(string args) + { + string[] parts = SplitTwo(args, " insert "); + if (parts.Length == 0) + { + return; + } + + if (!TryGetList(parts[0], out List list)) + { + return; + } + + string[] indexAndValue = SplitIndexAndValue(parts[1]); + if (indexAndValue.Length == 0) + { + return; + } + + if (!TryParseIndex(indexAndValue[0], out int index, list.Count + 1)) + { + return; + } + + list.Insert(index, indexAndValue[1]); + } + + private string[] SplitTwo(string input, string separator) + { + string[] parts = input.Split(separator, 2, StringSplitOptions.None); + if (parts.Length != 2) + { + RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true); + return []; + } + + parts[0] = parts[0].Trim(); + parts[1] = parts[1].Trim(); + + if (string.IsNullOrWhiteSpace(parts[0])) + { + RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true); + return []; + } + + return parts; + } + + private bool TryGetList(string name, out List list) + { + if (!RuntimeInfo.Lists.TryGetValue(name, out list)) + { + RuntimeInfo.Exit(ExitMessages.ListNotFound(name), true); + return false; + } + + return true; + } + + private string[] SplitIndexAndValue(string input) + { + string[] parts = input.Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length != 2) + { + RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true); + return []; + } + + parts[0] = parts[0].Trim(); + parts[1] = parts[1].Trim(); + return parts; + } + + private bool TryParseIndex(string rawIndex, out int index, int maxExclusive) + { + bool success = int.TryParse(rawIndex.Trim(), out index); + if (!success) + { + RuntimeInfo.Exit(ExitMessages.InvalidIndex(rawIndex), true); + return false; + } + + if (index < 0 || index >= maxExclusive) + { + RuntimeInfo.Exit(ExitMessages.IndexOutOfRange(index), true); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/YesNt.Interpreter/Statements/PredefinedVariableStatements.cs b/YesNt.Interpreter/Statements/PredefinedVariableStatements.cs new file mode 100644 index 0000000..528880b --- /dev/null +++ b/YesNt.Interpreter/Statements/PredefinedVariableStatements.cs @@ -0,0 +1,49 @@ +using System; + +using YesNt.Interpreter.Attributes; +using YesNt.Interpreter.Enums; +using YesNt.Interpreter.Runtime; +using YesNt.Interpreter.Utilities; + +namespace YesNt.Interpreter.Statements; + +internal class PredefinedVariableStatements : StatementRuntimeInformation +{ + private readonly Random random = new Random(); + + [Statement("%time", SearchMode.Contains, SpaceAround.None, ConsoleColor.Blue, KeepStatementInArgs = true, Priority = Priority.Highest)] + public void GetUnixTimestamp(string args) + { + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessSimplePlaceholders(args, "%time", DateTimeOffset.Now.ToUnixTimeSeconds().ToString()).TrimEnd(); + } + + [Statement("%os", SearchMode.Contains, SpaceAround.None, ConsoleColor.Blue, KeepStatementInArgs = true, Priority = Priority.Highest)] + public void GetOperatingSystem(string args) + { + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessSimplePlaceholders(args, "%os", Environment.OSVersion.Platform.ToString()).TrimEnd(); + } + + [Statement("%cpu", SearchMode.Contains, SpaceAround.None, ConsoleColor.Blue, KeepStatementInArgs = true, Priority = Priority.Highest)] + public void GetProcessorArchitecture(string args) + { + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessSimplePlaceholders(args, "%cpu", System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture.ToString()).TrimEnd(); + } + + [Statement("%is64", SearchMode.Contains, SpaceAround.None, ConsoleColor.Blue, KeepStatementInArgs = true, Priority = Priority.Highest)] + public void GetIsOperatingSystem64Bit(string args) + { + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessSimplePlaceholders(args, "%is64", Environment.Is64BitOperatingSystem.ToString()).TrimEnd(); + } + + [Statement("%pi", SearchMode.Contains, SpaceAround.None, ConsoleColor.Blue, KeepStatementInArgs = true, Priority = Priority.Highest)] + public void GetPi(string args) + { + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessSimplePlaceholders(args, "%pi", Math.PI.ToString()).TrimEnd(); + } + + [Statement("%rand", SearchMode.Contains, SpaceAround.None, ConsoleColor.Blue, KeepStatementInArgs = true, Priority = Priority.Highest)] + public void GetRandom(string args) + { + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessDynamicPlaceholders(args, "%rand", () => random.Next(32767, int.MaxValue).ToString()).TrimEnd(); + } +} \ No newline at end of file diff --git a/YesNt.Interpreter/Statements/PredifinedVariableStatements.cs b/YesNt.Interpreter/Statements/PredifinedVariableStatements.cs deleted file mode 100644 index 6974940..0000000 --- a/YesNt.Interpreter/Statements/PredifinedVariableStatements.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; - -using YesNt.Interpreter.Attributes; -using YesNt.Interpreter.Enums; -using YesNt.Interpreter.Runtime; -using YesNt.Interpreter.Utilities; - -namespace YesNt.Interpreter.Statements -{ - internal class PredifinedVariableStatements : StatementRuntimeInformation - { - private readonly Random random = new Random(); - - [Statement("%tim", SearchMode.Contains, SpaceAround.None, ConsoleColor.Blue, KeepStatementInArgs = true, Priority = Priority.Highest)] - public void GetUnixTimestamp(string args) - { - while (args.Contains("%tim")) - { - args = args.ReplaceFirstOccurrence("%tim", $"{DateTimeOffset.Now.ToUnixTimeSeconds()}"); - } - - RuntimeInfo.CurrentLine = args.TrimEnd(); - } - - [Statement("%pi", SearchMode.Contains, SpaceAround.None, ConsoleColor.Blue, KeepStatementInArgs = true, Priority = Priority.Highest)] - public void GetPi(string args) - { - args = args.Replace("%pi", $"{Math.PI}"); - - RuntimeInfo.CurrentLine = args.TrimEnd(); - } - - [Statement("%rnd", SearchMode.Contains, SpaceAround.None, ConsoleColor.Blue, KeepStatementInArgs = true, Priority = Priority.Highest)] - public void GetRandom(string args) - { - while (args.Contains("%rnd")) - { - args = args.ReplaceFirstOccurrence("%rnd", $"{random.Next(32767, int.MaxValue)}"); - } - - RuntimeInfo.CurrentLine = args.TrimEnd(); - } - } -} \ No newline at end of file diff --git a/YesNt.Interpreter/Statements/ProcessingStatements.cs b/YesNt.Interpreter/Statements/ProcessingStatements.cs index 282bcad..c94aa83 100644 --- a/YesNt.Interpreter/Statements/ProcessingStatements.cs +++ b/YesNt.Interpreter/Statements/ProcessingStatements.cs @@ -9,110 +9,98 @@ using YesNt.Interpreter.Enums; using YesNt.Interpreter.Runtime; using YesNt.Interpreter.Utilities; -namespace YesNt.Interpreter.Statements +namespace YesNt.Interpreter.Statements; + +internal partial class ProcessingStatements : StatementRuntimeInformation { - internal class ProcessingStatements : StatementRuntimeInformation + [Statement("calc", SearchMode.EndOfLine, SpaceAround.Start, ConsoleColor.DarkYellow, Priority = Priority.High)] + public void Calculate(string args) { - private static readonly Regex calculationRegex = new Regex(@"[0-9*+().,^%/-]+[0-9*+ ().,^%/-]+[0-9*+().,^%/-]+"); + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessCalculations(args.FromSafeString(), RuntimeInfo, CalculationRegex()); + } - [Statement("!calc", SearchMode.EndOfLine, SpaceAround.Start, ConsoleColor.DarkYellow, Priority = Priority.High)] - public void Calculate(string args) + [Statement("eval", SearchMode.EndOfLine, SpaceAround.Start, ConsoleColor.DarkYellow, Priority = Priority.VeryHigh)] + public void Evaluate(string args) + { + RuntimeInfo.CurrentLine = args.FromSafeString(); + } + + [Statement("task", SearchMode.EndOfLine, SpaceAround.Start, ConsoleColor.DarkYellow, Priority = Priority.VeryHigh)] + public void RunTask(string line) + { + int lineNumber = RuntimeInfo.LineNumber; + List lines = RuntimeInfo.Lines.GetRange(0, RuntimeInfo.Lines.Count); + + Line oldLine = lines[lineNumber]; + + lines[lineNumber] = new Line(line, oldLine.FileName, oldLine.LineNumber); + _ = Task.Run(() => { - MatchCollection matches = calculationRegex.Matches(args.FromSaveString()); + YesNtInterpreter interpreter = new YesNtInterpreter(); + interpreter.Execute(lines, RuntimeInfo.GlobalVariables, lineNumber, RuntimeInfo); + }); - for (int i = 0; i < matches.Count; i++) - { - string res = Evaluator.Calculate(matches[i].Value); - if (res is null) - { - RuntimeInfo.Exit("Invalid operation", true); - return; - } - args = args.FromSaveString().Replace(matches[i].Value, res); - } + RuntimeInfo.CurrentLine = string.Empty; + } - RuntimeInfo.CurrentLine = args; + [Statement("sleep", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Magenta)] + public void Sleep(string args) + { + if (int.TryParse(args, out int millisecondsTimeout)) + { + ConsoleExtensions.Sleep(millisecondsTimeout, RuntimeInfo); } - - [Statement("!eval", SearchMode.EndOfLine, SpaceAround.Start, ConsoleColor.DarkYellow, Priority = Priority.VeryHigh)] - public void Evaluate(string args) + else { - RuntimeInfo.CurrentLine = args.FromSaveString(); - } - - [Statement("!!", SearchMode.Contains, SpaceAround.None, ConsoleColor.DarkYellow, Priority = Priority.PreProcessing, KeepStatementInArgs = true)] - public void DontEvaluate(string args) - { - int index; - while ((index = args.IndexOf("!!")) != -1) - { - args = args.Remove(index, 2); - if (index < args.Length) - { - char charToEscape = args[index]; - args = args.Remove(index, 1); - args = args.Insert(index, charToEscape.ToString().ToSaveString()); - } - } - RuntimeInfo.CurrentLine = args; - } - - [Statement("!task", SearchMode.EndOfLine, SpaceAround.Start, ConsoleColor.DarkYellow, Priority = Priority.VeryHigh)] - public void RunTask(string line) - { - int lineNumer = RuntimeInfo.LineNumber; - List lines = RuntimeInfo.Lines.GetRange(0, RuntimeInfo.Lines.Count); - - Line oldLine = lines[lineNumer]; - - lines[lineNumer] = new Line(line, oldLine.FileName, oldLine.LineNumber); - _ = Task.Run(() => - { - YesNtInterpreter interpreter = new YesNtInterpreter(); - interpreter.Initialize(); - interpreter.Execute(lines, RuntimeInfo.GloablVariables, lineNumer, RuntimeInfo); - }); - - RuntimeInfo.CurrentLine = string.Empty; - } - - [Statement("slp", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Magenta)] - public void Sleep(string args) - { - _ = int.TryParse(args, out int millisecondsTimeout); - ConsoleExtentions.Sleep(millisecondsTimeout, RuntimeInfo); - } - - [Statement("imp", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Magenta)] - public void Import(string path) - { - if (string.IsNullOrEmpty(Path.GetExtension(path))) - { - path = Path.ChangeExtension(path, "ynt"); - } - - if (File.Exists(path)) - { - try - { - RuntimeInfo.Lines.RemoveAt(RuntimeInfo.LineNumber); - string[] lines = File.ReadAllLines(path); - - for (int i = 0; i < lines.Length; i++) - { - RuntimeInfo.Lines.Insert(RuntimeInfo.LineNumber + i, new Line(lines[i], Path.GetFileName(path), i)); - } - RuntimeInfo.LineNumber--; - } - catch - { - RuntimeInfo.Exit($"Could not load file \"{path}\"", true); - } - } - else - { - RuntimeInfo.Exit($"Could not find file \"{path}\"", true); - } + RuntimeInfo.Exit(ExitMessages.InvalidTimeoutValue(args), true); } } + + [Statement("length", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Magenta)] + public void Length(string args) + { + RuntimeInfo.InParametersStack.Clear(); + RuntimeInfo.OutParametersStack.Clear(); + + RuntimeInfo.OutParametersStack.Push(args.FromSafeString().Length.ToString()); + } + + [Statement("import", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Magenta)] + public void Import(string path) + { + path = path.FromSafeString(); + path = Path.Combine(RuntimeInfo.WorkingDirectory, path); + + if (string.IsNullOrEmpty(Path.GetExtension(path))) + { + path = Path.ChangeExtension(path, "ynt"); + } + + if (File.Exists(path)) + { + try + { + RuntimeInfo.Lines.RemoveAt(RuntimeInfo.LineNumber); + string[] lines = File.ReadAllLines(path); + + for (int i = 0; i < lines.Length; i++) + { + RuntimeInfo.Lines.Insert(RuntimeInfo.LineNumber + i, new Line(lines[i], Path.GetFileName(path), i)); + } + RuntimeInfo.PreScanLinesAction?.Invoke(); + RuntimeInfo.LineNumber--; + } + catch + { + RuntimeInfo.Exit(ExitMessages.CouldNotLoadFile(path), true); + } + } + else + { + RuntimeInfo.Exit(ExitMessages.CouldNotFindFile(path), true); + } + } + + [GeneratedRegex("[0-9*+().,^%/-]+[0-9*+ ().,^%/-]+[0-9*+().,^%/-]+")] + private static partial Regex CalculationRegex(); } \ No newline at end of file diff --git a/YesNt.Interpreter/Statements/StringLiteralStatements.cs b/YesNt.Interpreter/Statements/StringLiteralStatements.cs new file mode 100644 index 0000000..709a524 --- /dev/null +++ b/YesNt.Interpreter/Statements/StringLiteralStatements.cs @@ -0,0 +1,84 @@ +using System.Text; + +using YesNt.Interpreter.Attributes; +using YesNt.Interpreter.Enums; +using YesNt.Interpreter.Runtime; +using YesNt.Interpreter.Utilities; + +namespace YesNt.Interpreter.Statements; + +internal class StringLiteralStatements : StatementRuntimeInformation +{ + [Statement("\"", SearchMode.Contains, SpaceAround.None, System.ConsoleColor.DarkYellow, Priority = Priority.PreProcessing, KeepStatementInArgs = true)] + public void ParseStringLiterals(string args) + { + if (!args.Contains('"')) + { + return; + } + + StringBuilder output = new StringBuilder(args.Length); + +#pragma warning disable S127 // i is intentionally advanced to track position within quoted literals and escape sequences + for (int i = 0; i < args.Length; i++) + { + char current = args[i]; + if (current != '"') + { + _ = output.Append(current); + continue; + } + + StringBuilder literal = new StringBuilder(); + bool closed = false; + i++; + + for (; i < args.Length; i++) + { + char ch = args[i]; + if (ch == '\\' && i + 1 < args.Length) + { + i++; + _ = literal.Append(ParseEscape(args[i])); + continue; + } + + if (ch == '"') + { + closed = true; + break; + } + + _ = literal.Append(ch); + } +#pragma warning restore S127 + + if (!closed) + { + RuntimeInfo.Exit(ExitMessages.InvalidStringLiteral, true); + return; + } + + _ = output.Append(literal.ToString().ToSafeString()); + } + + RuntimeInfo.CurrentLine = output.ToString(); + } + + private static char ParseEscape(char escapeChar) + { + return escapeChar switch + { + 'n' => '\n', + 'r' => '\r', + 't' => '\t', + 'b' => '\b', + 'f' => '\f', + 'a' => '\a', + 'v' => '\v', + '"' => '"', + '\\' => '\\', + _ => escapeChar + }; + } +} \ No newline at end of file diff --git a/YesNt.Interpreter/Statements/SystemStatements.cs b/YesNt.Interpreter/Statements/SystemStatements.cs new file mode 100644 index 0000000..3b46cbb --- /dev/null +++ b/YesNt.Interpreter/Statements/SystemStatements.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.IO; +using System.Linq; + +using YesNt.Interpreter.Attributes; +using YesNt.Interpreter.Enums; +using YesNt.Interpreter.Runtime; +using YesNt.Interpreter.Utilities; + +namespace YesNt.Interpreter.Statements; + +internal class SystemStatements : StatementRuntimeInformation +{ + [Statement("exec", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Magenta, Priority = Priority.Low, Separator = " with ")] + public void ExecuteProgramWithArgs(string input) + { + string[] parts = input.FromSafeString().Split(" with ", 2, StringSplitOptions.None); + string program = parts[0].Trim(); + + foreach (string argument in parts[1].Split(',')) + { + RuntimeInfo.InParametersStack.Push(argument.Trim()); + } + + try + { + StartProcess(program, string.Join(" ", RuntimeInfo.InParametersStack.Reverse())); + } + catch (FileNotFoundException) + { + RuntimeInfo.Exit(ExitMessages.CannotFindFile(program), false); + } + catch (Win32Exception ex) + { + RuntimeInfo.Exit(ExitMessages.FailedToStart(program, ex.Message), false); + } + } + + [Statement("exec", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Magenta, Priority = Priority.VeryLow)] + public void ExecuteProgram(string input) + { + // This is to prevent the "exec with" statement from being triggered by this one, since they both start with "exec" + if (input.Contains(" with ")) + { + return; + } + + input = input.FromSafeString(); + + try + { + StartProcess(input, string.Join(" ", RuntimeInfo.InParametersStack.Reverse())); + } + catch (FileNotFoundException) + { + RuntimeInfo.Exit(ExitMessages.CannotFindFile(input), false); + } + catch (Win32Exception ex) + { + RuntimeInfo.Exit(ExitMessages.FailedToStart(input, ex.Message), false); + } + } + + private void Process_ErrorDataReceived(Utilities.DataReceivedEventArgs e, Stack outputStack) + { + if (string.IsNullOrWhiteSpace(e.Data)) + { + return; + } + + outputStack.Push(e.Data.ToSafeString()); + RuntimeInfo.Write("Error: " + e.Data); + } + + private void Process_OutputDataReceived(Utilities.DataReceivedEventArgs e, Stack outputStack) + { + if (string.IsNullOrWhiteSpace(e.Data)) + { + return; + } + + outputStack.Push(e.Data.ToSafeString()); + RuntimeInfo.Write(e.Data); + } + + private void StartProcess(string name, string args) + { + RuntimeInfo.OutParametersStack.Clear(); + + Stack outputStack = new Stack(); + + FixedProcess process = new FixedProcess + { + StartInfo = new ProcessStartInfo() + { + FileName = name, + Arguments = args, + RedirectStandardOutput = true, + RedirectStandardError = true + } + }; + + process.OutputDataReceived += (s, e) => Process_OutputDataReceived(e, outputStack); + process.ErrorDataReceived += (s, e) => Process_ErrorDataReceived(e, outputStack); + + _ = process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + process.WaitForExit(); + + RuntimeInfo.InParametersStack.Clear(); + + RuntimeInfo.OutParametersStack = new(outputStack); + RuntimeInfo.OutParametersStack.Push(process.ExitCode.ToString()); + } +} \ No newline at end of file diff --git a/YesNt.Interpreter/Statements/VariableStatements.cs b/YesNt.Interpreter/Statements/VariableStatements.cs index 8716075..f69329c 100644 --- a/YesNt.Interpreter/Statements/VariableStatements.cs +++ b/YesNt.Interpreter/Statements/VariableStatements.cs @@ -1,121 +1,67 @@ -using System.Collections.Generic; -using System.Text.RegularExpressions; +using System.Collections.Generic; using YesNt.Interpreter.Attributes; using YesNt.Interpreter.Enums; using YesNt.Interpreter.Runtime; +using YesNt.Interpreter.Utilities; -namespace YesNt.Interpreter.Statements +namespace YesNt.Interpreter.Statements; + +internal partial class VariableStatements : StatementRuntimeInformation { - internal class VariableStatements : StatementRuntimeInformation + [Statement("var", SearchMode.StartOfLine, SpaceAround.End, System.ConsoleColor.DarkBlue, Priority = Priority.VeryLow, Separator = "=")] + public void DefineVariable(string args) { - [Statement("<", SearchMode.StartOfLine, SpaceAround.None, Priority = Priority.VeryLow)] - public void DefineVariable(string args) - { - string[] parts = args.Split('='); - if (parts.Length == 2) - { - string key = parts[0].Trim(); - if (key.Contains(' ')) - { - RuntimeInfo.Exit("Invalid Syntax", true); - } + DefineVariableIn(RuntimeInfo.Variables, args); + } - if (RuntimeInfo.Variables.ContainsKey(key)) - { - RuntimeInfo.Variables[key] = parts[1].Trim(); - } - else - { - RuntimeInfo.Variables.Add(key, parts[1].Trim()); - } - } - else + [Statement("global", SearchMode.StartOfLine, SpaceAround.End, System.ConsoleColor.DarkBlue, Priority = Priority.VeryLow, Separator = "=")] + public void DefineGlobalVariable(string args) + { + DefineVariableIn(RuntimeInfo.GlobalVariables, args); + } + + private void DefineVariableIn(Dictionary dict, string args) + { + string[] parts = args.Split('='); + if (parts.Length == 2) + { + string key = parts[0].Trim(); + if (key.Contains(' ')) { - RuntimeInfo.Exit("Invalid syntax", true); + RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true); } + + dict[key] = parts[1].Trim(); } - - [Statement("!<", SearchMode.StartOfLine, SpaceAround.None, Priority = Priority.VeryLow)] - public void DefineGlobalVariable(string args) + else { - string[] parts = args.Split('='); - if (parts.Length == 2) - { - string key = parts[0].Trim(); - if (key.Contains(' ')) - { - RuntimeInfo.Exit("Invalid Syntax", true); - } - - if (RuntimeInfo.GloablVariables.ContainsKey(key)) - { - RuntimeInfo.GloablVariables[key] = parts[1].Trim(); - } - else - { - RuntimeInfo.GloablVariables.Add(key, parts[1].Trim()); - } - } - else - { - RuntimeInfo.Exit("Invalid syntax", true); - } - } - - [Statement("del", SearchMode.StartOfLine, SpaceAround.End, System.ConsoleColor.Red, Priority = Priority.VeryLow)] - public void DeleteVariable(string args) - { - string key = args.Trim(); - - if (RuntimeInfo.Variables.ContainsKey(key)) - { - RuntimeInfo.Variables.Remove(key); - } - else if (RuntimeInfo.GloablVariables.ContainsKey(key)) - { - RuntimeInfo.GloablVariables.Remove(key); - } - else - { - RuntimeInfo.Exit($"Variable \"{key}\" not found", true); - } - } - - [Statement(">", SearchMode.Contains, SpaceAround.None, Priority = Priority.Highest)] - public void ReadVariable(string _) - { - if (!RuntimeInfo.CurrentLine.Contains('>')) - { - return; - } - - foreach (KeyValuePair variable in RuntimeInfo.Variables) - { - RuntimeInfo.CurrentLine = RuntimeInfo.CurrentLine.Replace($">{variable.Key}", variable.Value); - } - - foreach (KeyValuePair variable in RuntimeInfo.GloablVariables) - { - RuntimeInfo.CurrentLine = RuntimeInfo.CurrentLine.Replace($">{variable.Key}", variable.Value); - } - - if (RuntimeInfo.IsSearching) - { - return; - } - - MatchCollection matches = Regex.Matches(RuntimeInfo.CurrentLine, @">[a-zA-Z0-9]+"); - - for (int i = 0; i < matches.Count; i++) - { - string varName = matches[i].Value.Replace(">", string.Empty); - if (!RuntimeInfo.Variables.ContainsKey(varName) && !RuntimeInfo.GloablVariables.ContainsKey(varName)) - { - RuntimeInfo.Exit($"Variable \"{varName}\" not found", true); - return; - } - } + RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true); } } + + [Statement("delete", SearchMode.StartOfLine, SpaceAround.End, System.ConsoleColor.Red, Priority = Priority.VeryLow)] + public void DeleteVariable(string args) + { + string key = args.Trim(); + + if (RuntimeInfo.Variables.ContainsKey(key)) + { + _ = RuntimeInfo.Variables.Remove(key); + } + else if (RuntimeInfo.GlobalVariables.ContainsKey(key)) + { + _ = RuntimeInfo.GlobalVariables.Remove(key); + } + else + { + RuntimeInfo.Exit(ExitMessages.VariableNotFound(key), true); + } + } + + [Statement("${", SearchMode.Contains, SpaceAround.None, Priority = Priority.Highest, Separator = "}")] + public void ReadVariable(string _) + { + RuntimeInfo.CurrentLine = TemplateProcessor.ProcessVariables(RuntimeInfo.CurrentLine, RuntimeInfo); + } } \ No newline at end of file diff --git a/YesNt.Interpreter/Utilities/ConsoleExtensions.cs b/YesNt.Interpreter/Utilities/ConsoleExtensions.cs new file mode 100644 index 0000000..f401e52 --- /dev/null +++ b/YesNt.Interpreter/Utilities/ConsoleExtensions.cs @@ -0,0 +1,37 @@ +using System; +using System.Diagnostics; +using System.Threading; + +using YesNt.Interpreter.Runtime; + +namespace YesNt.Interpreter.Utilities; + +internal static class ConsoleExtensions +{ + public static char ReadKey(RuntimeInformation runtimeInformation) + { + while (!runtimeInformation.Stop) + { + if (Console.KeyAvailable) + { + return Console.ReadKey().KeyChar; + } + Thread.Sleep(10); + } + return ' '; + } + + public static void Sleep(int millisecondsTimeout, RuntimeInformation runtimeInformation) + { + Stopwatch stopwatch = new Stopwatch(); + stopwatch.Start(); + while (!runtimeInformation.Stop) + { + if (stopwatch.ElapsedMilliseconds > millisecondsTimeout) + { + return; + } + Thread.Sleep(10); + } + } +} \ No newline at end of file diff --git a/YesNt.Interpreter/Utilities/ConsoleExtentions.cs b/YesNt.Interpreter/Utilities/ConsoleExtentions.cs deleted file mode 100644 index a394935..0000000 --- a/YesNt.Interpreter/Utilities/ConsoleExtentions.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System; -using System.Diagnostics; -using System.Threading; - -using YesNt.Interpreter.Runtime; - -namespace YesNt.Interpreter.Utilities -{ - internal static class ConsoleExtentions - { - public static char ReadKey(RuntimeInformation runtimeInformation) - { - while (!runtimeInformation.Stop) - { - if (Console.KeyAvailable) - { - return Console.ReadKey().KeyChar; - } - Thread.Sleep(10); - } - return ' '; - } - - public static void Sleep(int millisecondsTimeout, RuntimeInformation runtimeInformation) - { - Stopwatch stopwatch = new Stopwatch(); - stopwatch.Start(); - while (!runtimeInformation.Stop) - { - if (stopwatch.ElapsedMilliseconds > millisecondsTimeout) - { - return; - } - Thread.Sleep(10); - } - } - } -} \ No newline at end of file diff --git a/YesNt.Interpreter/Utilities/Evaluator.cs b/YesNt.Interpreter/Utilities/Evaluator.cs index 1f0f239..9252d7d 100644 --- a/YesNt.Interpreter/Utilities/Evaluator.cs +++ b/YesNt.Interpreter/Utilities/Evaluator.cs @@ -1,198 +1,217 @@ -using System; +using System; using System.Linq; using System.Text.RegularExpressions; -namespace YesNt.Interpreter.Utilities +namespace YesNt.Interpreter.Utilities; + +/// +/// Provides expression evaluation used by conditional and arithmetic statements. +/// +internal static partial class Evaluator { - internal static class Evaluator + /// + /// Evaluates a boolean condition string such as a == b, x > 3, or true. + /// + /// The condition expression, which may contain safe-string encoded values. + /// + /// or if the condition could be evaluated; + /// if the expression is not a recognized condition form (treated as an error by callers). + /// + public static bool? EvaluateCondition(string input) { - public static bool? EvaluateCondition(string input) + input = input.FromSafeString(); + string lower = input.ToLower().Trim(); + if (lower == "true") { - if (input.ToLower().FromSaveString().Trim() == "true") - { - return true; - } - else if (input.ToLower().FromSaveString().Trim() == "false") - { - return false; - } + return true; + } + else if (lower == "false") + { + return false; + } - string[] parts = input.Split("=="); - if (parts.Length == 2) - { - string part1 = parts[0].FromSaveString().Trim(); - string part2 = parts[1].FromSaveString().Trim(); - return part1 == part2; - } + string[] parts = input.Split("=="); + if (parts.Length == 2) + { + string part1 = parts[0].Trim(); + string part2 = parts[1].Trim(); + return part1 == part2; + } - parts = input.Split("!="); - if (parts.Length == 2) - { - string part1 = parts[0].FromSaveString().Trim(); - string part2 = parts[1].FromSaveString().Trim(); - return part1 != part2; - } + parts = input.Split("!="); + if (parts.Length == 2) + { + string part1 = parts[0].Trim(); + string part2 = parts[1].Trim(); + return part1 != part2; + } - parts = input.Split(">="); - if (parts.Length == 2) - { - bool succ1 = parts[0].ToStandardizedNumber(out double part1); - bool succ2 = parts[1].ToStandardizedNumber(out double part2); - if (!succ1 || !succ2) - { - return false; - } + parts = input.Split(">="); + if (parts.Length == 2) + { + bool succ1 = parts[0].ToStandardizedNumber(out double part1); + bool succ2 = parts[1].ToStandardizedNumber(out double part2); + return succ1 && succ2 && part1 >= part2; + } - return part1 >= part2; - } + parts = input.Split("<="); + if (parts.Length == 2) + { + bool succ1 = parts[0].ToStandardizedNumber(out double part1); + bool succ2 = parts[1].ToStandardizedNumber(out double part2); + return succ1 && succ2 && part1 <= part2; + } - parts = input.Split("<="); - if (parts.Length == 2) - { - bool succ1 = parts[0].ToStandardizedNumber(out double part1); - bool succ2 = parts[1].ToStandardizedNumber(out double part2); - if (!succ1 || !succ2) - { - return false; - } + parts = input.Split(">"); + if (parts.Length == 2) + { + bool succ1 = parts[0].ToStandardizedNumber(out double part1); + bool succ2 = parts[1].ToStandardizedNumber(out double part2); + return succ1 && succ2 && part1 > part2; + } - return part1 <= part2; - } + parts = input.Split("<"); + if (parts.Length == 2) + { + bool succ1 = parts[0].ToStandardizedNumber(out double part1); + bool succ2 = parts[1].ToStandardizedNumber(out double part2); + return succ1 && succ2 && part1 < part2; + } - parts = input.Split(">"); - if (parts.Length == 2) - { - bool succ1 = parts[0].ToStandardizedNumber(out double part1); - bool succ2 = parts[1].ToStandardizedNumber(out double part2); - if (!succ1 || !succ2) - { - return false; - } + return null; + } - return part1 > part2; - } + /// + /// Evaluates a numeric arithmetic expression string and returns the result as a string. + /// Supports +, -, *, /, % (modulo), and ^ (power) operators + /// with standard precedence (^ highest, +/- lowest) and parentheses. + /// Adjacent sign characters (++, --, -+, +-) are normalized before evaluation. + /// + /// The arithmetic expression to evaluate. + /// The result as a culture-invariant numeric string, or "NaN" if evaluation failed. + public static string Calculate(string input) + { + input = input.FromSafeString(); + input = PlusPlusRegex().Replace(input, "+"); + input = MinusMinusRegex().Replace(input, "+"); + input = MinusPlusRegex().Replace(input, "-"); + input = PlusMinusRegex().Replace(input, "-"); - parts = input.Split("<"); - if (parts.Length == 2) - { - bool succ1 = parts[0].ToStandardizedNumber(out double part1); - bool succ2 = parts[1].ToStandardizedNumber(out double part2); - if (!succ1 || !succ2) - { - return false; - } - - return part1 < part2; - } + return CalculateInternal(input, '+'); + } + private static string CalculateInternal(string input, char op) + { + if (string.IsNullOrWhiteSpace(input)) + { return null; } - public static string Calculate(string input) + if (input.ToStandardizedNumber(out double quickNum)) { - input = Regex.Replace(input, @"(\+ +\+)+", "+"); - input = Regex.Replace(input, @"(\- +\-)+", "+"); - input = Regex.Replace(input, @"(\- +\+)+", "-"); - input = Regex.Replace(input, @"(\+ +\-)+", "-"); - - string yes = Calculate(input, '+'); - return yes; + return quickNum.ToString(System.Globalization.CultureInfo.InvariantCulture); } - private static string Calculate(string input, char op) + MatchCollection matches = ParenthesesRegex().Matches(input); + while (matches.Count > 0) { - if (input is null) + for (int i = 0; i < matches.Count; i++) + { + string calc = matches[i].Value.Substring(1, matches[i].Length - 2); + string ret = CalculateInternal(calc, '+'); + input = input.Replace(matches[i].Value, ret); + } + matches = ParenthesesRegex().Matches(input); + } + + string[] parts = input.Split(op); + + // If the expression starts with the operator (e.g. "-3 + 5" split by '-' gives ["", "3 + 5"]), + // prepend the operator back onto the first real part so it isn't lost. + if (parts.Length >= 2 && string.IsNullOrWhiteSpace(parts[0])) + { + parts[1] = $"{op}{parts[1]}"; + parts = parts.Skip(1).ToArray(); + } + + double number = double.NaN; + + foreach (string p in parts) + { + string part = p; + + part = op switch + { + '+' => CalculateInternal(part, '-'), + '-' => CalculateInternal(part, '*'), + '*' => CalculateInternal(part, '/'), + '/' => CalculateInternal(part, '%'), + '%' => CalculateInternal(part, '^'), + _ => part + }; + + if (part is null) { return null; } - input = input.FromSaveString(); - - MatchCollection matches = Regex.Matches(input, @"\(([^()]+)\)"); - while (matches.Count > 0) + if (part.ToStandardizedNumber(out double num)) { - for (int i = 0; i < matches.Count; i++) + if (double.IsNaN(number)) { - string calc = matches[i].Value.Substring(1, matches[i].Length - 2); - string ret = Calculate(calc); - input = input.Replace(matches[i].Value, ret); - } - matches = Regex.Matches(input, @"\(([^()]+)\)"); - } - - string[] parts = input.Split(op); - - //Weird fix - if (parts.Length >= 2 && string.IsNullOrWhiteSpace(parts[0])) - { - parts[1] = $"{op}{parts[1]}"; - parts = parts.Skip(1).ToArray(); - } - - double number = double.NaN; - - foreach (string p in parts) - { - string part = p; - - part = op switch - { - '+' => Calculate(part, '-'), - '-' => Calculate(part, '*'), - '*' => Calculate(part, '/'), - '/' => Calculate(part, '%'), - '%' => Calculate(part, '^'), - _ => part - }; - - if (part is null) - { - return null; - } - - if (part.ToStandardizedNumber(out double num)) - { - if (double.IsNaN(number)) - { - number = num; - } - else - { - switch (op) - { - case '+': - number += num; - break; - - case '-': - number -= num; - break; - - case '*': - number *= num; - break; - - case '/': - number /= num; - break; - - case '%': - number %= num; - break; - - case '^': - number = Math.Pow(number, num); - break; - } - } + number = num; } else { - return null; + switch (op) + { + case '+': + number += num; + break; + + case '-': + number -= num; + break; + + case '*': + number *= num; + break; + + case '/': + number /= num; + break; + + case '%': + number %= num; + break; + + case '^': + number = Math.Pow(number, num); + break; + } } } - - return number.ToString(System.Globalization.CultureInfo.InvariantCulture); + else + { + return null; + } } + + return number.ToString(System.Globalization.CultureInfo.InvariantCulture); } + + [GeneratedRegex("\\(([^()]+)\\)")] + private static partial Regex ParenthesesRegex(); + + [GeneratedRegex("(\\+ +\\+)+")] + private static partial Regex PlusPlusRegex(); + + [GeneratedRegex("(\\- +\\-)+")] + private static partial Regex MinusMinusRegex(); + + [GeneratedRegex("(\\- +\\+)+")] + private static partial Regex MinusPlusRegex(); + + [GeneratedRegex("(\\+ +\\-)+")] + private static partial Regex PlusMinusRegex(); } \ No newline at end of file diff --git a/YesNt.Interpreter/Utilities/FixedProcess.cs b/YesNt.Interpreter/Utilities/FixedProcess.cs new file mode 100644 index 0000000..5fd9ce6 --- /dev/null +++ b/YesNt.Interpreter/Utilities/FixedProcess.cs @@ -0,0 +1,323 @@ +using System; +using System.Collections; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading; + +namespace YesNt.Interpreter.Utilities; + +/// Represents the method that handles the and events. +public delegate void DataReceivedEventHandler(object sender, DataReceivedEventArgs e); + +internal delegate void UserCallBack(string data); + +/// +/// A workaround replacement for that fixes a buffering +/// issue in / : +/// the BCL implementation only delivers data when a newline is encountered, which means partial +/// lines are not raised until the process writes another newline or exits. +/// flushes whatever is in the read buffer immediately, enabling real-time +/// output forwarding for interactive child processes. +/// +internal class FixedProcess : Process +{ + public new event DataReceivedEventHandler OutputDataReceived; + + public new event DataReceivedEventHandler ErrorDataReceived; + + internal AsyncStreamReader output; + internal AsyncStreamReader error; + + public new void BeginOutputReadLine() + { + Stream baseStream = StandardOutput.BaseStream; + output = new AsyncStreamReader(baseStream, new UserCallBack(FixedOutputReadNotifyUser), StandardOutput.CurrentEncoding); + output.BeginReadLine(); + } + + public new void BeginErrorReadLine() + { + Stream baseStream = StandardError.BaseStream; + error = new AsyncStreamReader(baseStream, new UserCallBack(FixedErrorReadNotifyUser), StandardError.CurrentEncoding); + error.BeginReadLine(); + } + + internal void FixedOutputReadNotifyUser(string data) + { + DataReceivedEventHandler outputDataReceived = OutputDataReceived; + if (outputDataReceived != null) + { + DataReceivedEventArgs dataReceivedEventArgs = new DataReceivedEventArgs(data); + if (SynchronizingObject != null && SynchronizingObject.InvokeRequired) + { + _ = SynchronizingObject.Invoke(outputDataReceived, + [ + this, + dataReceivedEventArgs + ]); + return; + } + outputDataReceived(this, dataReceivedEventArgs); + } + } + + internal void FixedErrorReadNotifyUser(string data) + { + DataReceivedEventHandler errorDataReceived = ErrorDataReceived; + if (errorDataReceived != null) + { + DataReceivedEventArgs dataReceivedEventArgs = new DataReceivedEventArgs(data); + if (SynchronizingObject != null && SynchronizingObject.InvokeRequired) + { + _ = SynchronizingObject.Invoke(errorDataReceived, + [ + this, + dataReceivedEventArgs + ]); + return; + } + errorDataReceived(this, dataReceivedEventArgs); + } + } +} + +/// Provides data for the and events. +public class DataReceivedEventArgs : EventArgs +{ + internal string _data; + + /// Gets the line of characters that was written to a redirected output stream. + /// The line that was written by an associated to its redirected or stream. + /// 2 + public string Data => _data; + + internal DataReceivedEventArgs(string data) + { + _data = data; + } +} + +internal class AsyncStreamReader : IDisposable +{ + internal const int DefaultBufferSize = 1024; + private readonly Queue messageQueue; + private Stream stream; + private Encoding encoding; + private Decoder decoder; + private byte[] byteBuffer; + private char[] charBuffer; + private UserCallBack userCallBack; + private bool cancelOperation; + private ManualResetEvent eofEvent; + private StringBuilder sb; + private bool bLastCarriageReturn; + public virtual Encoding CurrentEncoding => encoding; + public virtual Stream BaseStream => stream; + + internal AsyncStreamReader(Stream stream, UserCallBack callback, Encoding encoding) : this(stream, callback, encoding, 1024) + { + } + + internal AsyncStreamReader(Stream stream, UserCallBack callback, Encoding encoding, int bufferSize) + { + Init(stream, callback, encoding, bufferSize); + messageQueue = new Queue(); + } + + public virtual void Close() + { + Dispose(true); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + internal void BeginReadLine() + { + if (cancelOperation) + { + cancelOperation = false; + } + if (sb == null) + { + sb = new StringBuilder(1024); + _ = stream.BeginRead(byteBuffer, 0, byteBuffer.Length, new AsyncCallback(ReadBuffer), null); + return; + } + FlushMessageQueue(); + } + + internal void CancelOperation() + { + cancelOperation = true; + } + + internal void WaitUtilEOF() + { + if (eofEvent != null) + { + _ = eofEvent.WaitOne(); + eofEvent.Close(); + eofEvent = null; + } + } + + protected virtual void Dispose(bool disposing) + { + if (disposing && stream != null) + { + stream.Close(); + } + if (stream != null) + { + stream = null; + encoding = null; + decoder = null; + byteBuffer = null; + charBuffer = null; + } + + eofEvent?.Close(); + eofEvent = null; + } + + private void Init(Stream stream, UserCallBack callback, Encoding encoding, int bufferSize) + { + this.stream = stream; + this.encoding = encoding; + userCallBack = callback; + decoder = encoding.GetDecoder(); + if (bufferSize < 128) + { + bufferSize = 128; + } + byteBuffer = new byte[bufferSize]; + int _maxCharsPerBuffer = encoding.GetMaxCharCount(bufferSize); + charBuffer = new char[_maxCharsPerBuffer]; + cancelOperation = false; + eofEvent = new ManualResetEvent(false); + sb = null; + bLastCarriageReturn = false; + } + + private void ReadBuffer(IAsyncResult ar) + { + int num; + try + { + num = stream.EndRead(ar); + } + catch (IOException) + { + num = 0; + } + catch (OperationCanceledException) + { + num = 0; + } + if (num == 0) + { + lock (messageQueue) + { + if (sb.Length != 0) + { + messageQueue.Enqueue(sb.ToString()); + sb.Length = 0; + } + messageQueue.Enqueue(null); + } + try + { + FlushMessageQueue(); + return; + } + finally + { + _ = eofEvent.Set(); + } + } + int chars = decoder.GetChars(byteBuffer, 0, num, charBuffer, 0); + _ = sb.Append(charBuffer, 0, chars); + GetLinesFromStringBuilder(); + _ = stream.BeginRead(byteBuffer, 0, byteBuffer.Length, new AsyncCallback(ReadBuffer), null); + } + + private void GetLinesFromStringBuilder() + { + int i = 0; + int num = 0; + int length = sb.Length; + if (bLastCarriageReturn && length > 0 && sb[0] == '\n') + { + i = 1; + num = 1; + bLastCarriageReturn = false; + } + while (i < length) + { + char c = sb[i]; + if (c is '\r' or '\n') + { + if (c == '\r' && i + 1 < length && sb[i + 1] == '\n') + { + i++; + } + + string obj = sb.ToString(num, i + 1 - num); + + num = i + 1; + + lock (messageQueue) + { + messageQueue.Enqueue(obj); + } + } + i++; + } + + // Flush Fix: Send Whatever is left in the buffer + string endOfBuffer = sb.ToString(num, length - num); + lock (messageQueue) + { + messageQueue.Enqueue(endOfBuffer); + num = length; + } + // End Flush Fix + + if (sb[length - 1] == '\r') + { + bLastCarriageReturn = true; + } + if (num < length) + { + _ = sb.Remove(0, num); + } + else + { + sb.Length = 0; + } + FlushMessageQueue(); + } + + private void FlushMessageQueue() + { + while (messageQueue.Count > 0) + { + lock (messageQueue) + { + if (messageQueue.Count > 0) + { + string data = (string)messageQueue.Dequeue(); + if (!cancelOperation) + { + userCallBack(data); + } + } + } + } + } +} \ No newline at end of file diff --git a/YesNt.Interpreter/Utilities/StringExtensions.cs b/YesNt.Interpreter/Utilities/StringExtensions.cs new file mode 100644 index 0000000..9b69484 --- /dev/null +++ b/YesNt.Interpreter/Utilities/StringExtensions.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace YesNt.Interpreter.Utilities; + +/// +/// Extension methods for string manipulation used throughout the interpreter. +/// +/// +/// +/// YesNt uses a "safe string" encoding to pass values through the interpreter pipeline without +/// accidentally triggering keyword matching. Special characters (spaces, operators, punctuation, +/// control characters) are replaced with SOH-delimited three-letter codes +/// (e.g. space → \x01spc\x01, newline → \x01nli\x01). These codes use the +/// non-printable SOH character (U+0001) as a sentinel. Written via string concatenation +/// ("\x01" + "spc" + "\x01") to avoid C#'s greedy \x hex escape absorbing +/// following hex-digit letters. U+0001 cannot appear in normal user source, preventing raw +/// source text from being accidentally decoded. +/// The mapping is defined in . Use +/// to encode and to decode. +/// +/// +public static class StringExtensions +{ + /// + /// Gets the table that maps special characters to their safe-string escape codes. + /// Keys are the original characters; values are the three-letter tilde codes. + /// + public static Dictionary ReplacementRules { get; } = new() + { + {"~", "\x01" + "til" + "\x01" }, + {" ", "\x01" + "spc" + "\x01" }, + {"%", "\x01" + "per" + "\x01" }, + {"<", "\x01" + "let" + "\x01" }, + {">", "\x01" + "grt" + "\x01" }, + {",", "\x01" + "com" + "\x01" }, + {"!", "\x01" + "exm" + "\x01" }, + {"|", "\x01" + "pip" + "\x01" }, + {"\n", "\x01" + "nli" + "\x01" }, + {"\r", "\x01" + "ret" + "\x01" }, + {"\t", "\x01" + "tab" + "\x01" }, + {"\b", "\x01" + "bac" + "\x01" }, + {"\f", "\x01" + "for" + "\x01" }, + {"\a", "\x01" + "ale" + "\x01" }, + {"", "\x01" + "emp" + "\x01" }, + }; + + private static readonly Dictionary reverseReplacementRules = ReplacementRules.ToDictionary(x => x.Value, x => x.Key); + + /// + /// Encodes a string into safe-string format so that special characters cannot accidentally + /// trigger interpreter keyword matching. Each character is wrapped with vertical-tab sentinels + /// before rule substitution so that multi-character replacements do not overlap. + /// + /// The plain string to encode. + /// The safe-string encoded representation. + public static string ToSafeString(this string input) + { + if (string.IsNullOrEmpty(input)) + { + return input; + } + + StringBuilder output = new StringBuilder(input.Length * 3); + foreach (char c in input) + { + string s = c.ToString(); + if (ReplacementRules.TryGetValue(s, out string replacement)) + { + _ = output.Append('\v'); + _ = output.Append(replacement); + _ = output.Append('\v'); + } + else + { + _ = output.Append('\v'); + _ = output.Append(c); + _ = output.Append('\v'); + } + } + + return output.ToString(); + } + + /// + /// Decodes a safe-string back to its original plain-text form. + /// + /// A safe-string encoded string. + /// The decoded plain string. + public static string FromSafeString(this string input) + { + if (string.IsNullOrEmpty(input) || (!input.Contains('\v') && !input.Contains('\x01'))) + { + return input; + } + + string stripped = input.Replace("\v", string.Empty); + if (!stripped.Contains('\x01')) + { + return stripped; + } + + StringBuilder output = new StringBuilder(stripped.Length); +#pragma warning disable S127 // i is intentionally advanced by 4 when a 5-char escape code is consumed + for (int i = 0; i < stripped.Length; i++) + { + if (stripped[i] == '\x01' && i + 4 < stripped.Length && stripped[i + 4] == '\x01') + { + string code = stripped.Substring(i, 5); + if (reverseReplacementRules.TryGetValue(code, out string value)) + { + _ = output.Append(value); + i += 4; + continue; + } + } + + _ = output.Append(stripped[i]); + } +#pragma warning restore S127 + + return output.ToString(); + } + + /// + /// Tries to parse the string as a , first decoding safe-string encoding + /// and normalising decimal separators (comma → period). + /// + /// The string to parse (may be safe-string encoded). + /// When this method returns, contains the parsed value if successful. + /// if parsing succeeded; otherwise . + public static bool ToStandardizedNumber(this string input, out double result) + { + if (input.IndexOf(',') != -1) + { + input = input.Replace(',', '.'); + } + return double.TryParse(input, NumberStyles.Any, CultureInfo.InvariantCulture, out result); + } + + /// Replaces only the first occurrence of in the string. + /// The source string. + /// The substring to find. + /// The replacement value. + /// A new string with the first occurrence replaced. + public static string ReplaceFirstOccurrence(this string input, string oldValue, string newValue) + { + int place = input.IndexOf(oldValue); + return input.Remove(place, oldValue.Length).Insert(place, newValue); + } + + /// Replaces only the last occurrence of in the string. + /// The source string. + /// The substring to find. + /// The replacement value. + /// A new string with the last occurrence replaced. + public static string ReplaceLastOccurrence(this string input, string oldValue, string newValue) + { + int place = input.LastIndexOf(oldValue); + return input.Remove(place, Math.Min(oldValue.Length, input.Length - place)).Insert(place, newValue); + } + + /// Counts the number of trailing whitespace characters in the string. + /// The source string. + /// The number of whitespace characters at the end of the string. + public static int WhiteSpaceAtEnd(this string input) + { + int count = 0; + int index = input.Length - 1; + while (index >= 0 && char.IsWhiteSpace(input[index--])) + { + count++; + } + + return count; + } +} \ No newline at end of file diff --git a/YesNt.Interpreter/Utilities/StringExtentions.cs b/YesNt.Interpreter/Utilities/StringExtentions.cs deleted file mode 100644 index 6c81400..0000000 --- a/YesNt.Interpreter/Utilities/StringExtentions.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System; -using System.Globalization; -using System.Text; - -namespace YesNt.Interpreter.Utilities -{ - public static class StringExtentions - { - public static string ToSaveString(this string input) - { - StringBuilder output = new StringBuilder(); - foreach (char c in input) - { - output.Append($"\v{c}\v"); - } - return output.ToString(); - } - - public static string FromSaveString(this string input) - { - return input.Replace("\v", ""); - } - - public static bool ToStandardizedNumber(this string input, out double result) - { - return double.TryParse(input.FromSaveString().Replace(',', '.'), NumberStyles.Any, CultureInfo.InvariantCulture, out result); - } - - public static string ReplaceFirstOccurrence(this string input, string oldValue, string newValue) - { - int place = input.IndexOf(oldValue); - return input.Remove(place, oldValue.Length).Insert(place, newValue); - } - - public static string ReplaceLastOccurrence(this string input, string oldValue, string newValue) - { - int place = input.LastIndexOf(oldValue); - return input.Remove(place, Math.Min(oldValue.Length, input.Length - place)).Insert(place, newValue); - } - - public static int WhiteSpaceAtEnd(this string input) - { - int count = 0; - int index = input.Length - 1; - while (index >= 0 && char.IsWhiteSpace(input[index--])) - { - count++; - } - - return count; - } - } -} \ No newline at end of file diff --git a/YesNt.Interpreter/Utilities/TemplateProcessor.cs b/YesNt.Interpreter/Utilities/TemplateProcessor.cs new file mode 100644 index 0000000..5d497bd --- /dev/null +++ b/YesNt.Interpreter/Utilities/TemplateProcessor.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; + +using YesNt.Interpreter.Runtime; + +namespace YesNt.Interpreter.Utilities; + +/// +/// Provides high-performance template substitution for variables and stack parameters. +/// +internal static class TemplateProcessor +{ + /// + /// Replaces all occurrences of ${variableName} with their current values. + /// + public static string ProcessVariables(string input, RuntimeInformation runtimeInfo) + { + if (string.IsNullOrEmpty(input)) + { + return input; + } + + int startIdx = input.IndexOf("${", StringComparison.Ordinal); + if (startIdx == -1) + { + return input; + } + + StringBuilder sb = new StringBuilder(input.Length); + int lastIdx = 0; + + while (startIdx != -1) + { + _ = sb.Append(input, lastIdx, startIdx - lastIdx); + int endIdx = input.IndexOf('}', startIdx + 2); + if (endIdx == -1) + { + _ = sb.Append("${"); + lastIdx = startIdx + 2; + } + else + { + string varName = input[(startIdx + 2)..endIdx]; + if (runtimeInfo.Variables.TryGetValue(varName, out string value)) + { + _ = sb.Append(value); + } + else if (runtimeInfo.GlobalVariables.TryGetValue(varName, out value)) + { + _ = sb.Append(value); + } + else if (!runtimeInfo.IsSearching) + { + runtimeInfo.Exit(ExitMessages.VariableNotFound(varName), true); + return input; + } + else + { + _ = sb.Append("${"); + _ = sb.Append(varName); + _ = sb.Append('}'); + } + + lastIdx = endIdx + 1; + } + + startIdx = input.IndexOf("${", lastIdx, StringComparison.Ordinal); + } + + _ = sb.Append(input, lastIdx, input.Length - lastIdx); + return sb.ToString(); + } + + /// + /// Replaces all occurrences of a placeholder (e.g., %in, %out) with values popped from a stack. + /// + public static string ProcessStackParameters(string input, string placeholder, Stack stack, RuntimeInformation runtimeInfo, string emptyStackMessage) + { + if (string.IsNullOrEmpty(input)) + { + return input; + } + + int startIdx = input.IndexOf(placeholder, StringComparison.Ordinal); + if (startIdx == -1) + { + return input; + } + + StringBuilder sb = new StringBuilder(input.Length); + int lastIdx = 0; + int placeholderLen = placeholder.Length; + + while (startIdx != -1) + { + _ = sb.Append(input, lastIdx, startIdx - lastIdx); + if (stack.Count == 0) + { + runtimeInfo.Exit(emptyStackMessage, true); + return input; + } + _ = sb.Append(stack.Pop()); + lastIdx = startIdx + placeholderLen; + startIdx = input.IndexOf(placeholder, lastIdx, StringComparison.Ordinal); + } + + _ = sb.Append(input, lastIdx, input.Length - lastIdx); + return sb.ToString(); + } + + /// + /// Replaces all occurrences of a placeholder with a fixed value. + /// + public static string ProcessSimplePlaceholders(string input, string placeholder, string value) + { + return string.IsNullOrEmpty(input) ? input : input.Replace(placeholder, value, StringComparison.Ordinal); + } + + /// + /// Replaces all occurrences of a placeholder with values generated by a provider function. + /// + public static string ProcessDynamicPlaceholders(string input, string placeholder, Func valueProvider) + { + if (string.IsNullOrEmpty(input)) + { + return input; + } + + int startIdx = input.IndexOf(placeholder, StringComparison.Ordinal); + if (startIdx == -1) + { + return input; + } + + StringBuilder sb = new StringBuilder(input.Length); + int lastIdx = 0; + int placeholderLen = placeholder.Length; + + while (startIdx != -1) + { + _ = sb.Append(input, lastIdx, startIdx - lastIdx); + _ = sb.Append(valueProvider()); + lastIdx = startIdx + placeholderLen; + startIdx = input.IndexOf(placeholder, lastIdx, StringComparison.Ordinal); + } + + _ = sb.Append(input, lastIdx, input.Length - lastIdx); + return sb.ToString(); + } + + /// + /// Replaces all occurrences of arithmetic expressions with their results. + /// + public static string ProcessCalculations(string input, RuntimeInformation runtimeInfo, Regex calculationRegex) + { + if (string.IsNullOrEmpty(input)) + { + return input; + } + + MatchCollection matches = calculationRegex.Matches(input); + if (matches.Count == 0) + { + return input; + } + + StringBuilder sb = new StringBuilder(input.Length); + int lastIdx = 0; + + for (int i = 0; i < matches.Count; i++) + { + Match match = matches[i]; + _ = sb.Append(input, lastIdx, match.Index - lastIdx); + + string res = Evaluator.Calculate(match.Value); + if (res is null) + { + runtimeInfo.Exit(ExitMessages.InvalidOperation, true); + return input; + } + _ = sb.Append(res); + lastIdx = match.Index + match.Length; + } + + _ = sb.Append(input, lastIdx, input.Length - lastIdx); + return sb.ToString(); + } +} \ No newline at end of file diff --git a/YesNt.Interpreter/YesNt.Interpreter.csproj b/YesNt.Interpreter/YesNt.Interpreter.csproj index c017137..d280294 100644 --- a/YesNt.Interpreter/YesNt.Interpreter.csproj +++ b/YesNt.Interpreter/YesNt.Interpreter.csproj @@ -1,11 +1,61 @@ - + - net5.0 + net10.0 YesNt.Interpreter - Exe + Library + AnyCPU;x64 + True + README.md + https://github.com/Stone-Red-Code/YesNt-Interpreter/ + scripting, modding, language + Logo.png + True + LICENSE + + True + + + + True + + + + True + + + + True + + + + + True + \ + + + True + \ + + + True + \ + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/assets/Logo.png b/assets/Logo.png new file mode 100644 index 0000000..1ae3f41 Binary files /dev/null and b/assets/Logo.png differ diff --git a/assets/Logo.svg b/assets/Logo.svg new file mode 100644 index 0000000..a7685f0 --- /dev/null +++ b/assets/Logo.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/benchmarks/arith_loop.js b/benchmarks/arith_loop.js new file mode 100644 index 0000000..6019cb9 --- /dev/null +++ b/benchmarks/arith_loop.js @@ -0,0 +1,7 @@ +let i = 0; +let s = 0; +while (i < 200000) { + i += 1; + s += i; +} +console.log(s); diff --git a/benchmarks/arith_loop.py b/benchmarks/arith_loop.py new file mode 100644 index 0000000..e4010e8 --- /dev/null +++ b/benchmarks/arith_loop.py @@ -0,0 +1,6 @@ +i = 0 +s = 0 +while i < 200000: + i += 1 + s += i +print(s) diff --git a/benchmarks/arith_loop.ynt b/benchmarks/arith_loop.ynt new file mode 100644 index 0000000..1705e47 --- /dev/null +++ b/benchmarks/arith_loop.ynt @@ -0,0 +1,7 @@ +var i = 0 +var sum = 0 +while ${i} < 200000: + var i = ${i} + 1 calc + var sum = ${sum} + ${i} calc +end_while +print_line ${sum} diff --git a/benchmarks/func_call_loop.js b/benchmarks/func_call_loop.js new file mode 100644 index 0000000..e940613 --- /dev/null +++ b/benchmarks/func_call_loop.js @@ -0,0 +1,8 @@ +function add_one(x) { + return x + 1; +} +let i = 0; +while (i < 60000) { + i = add_one(i); +} +console.log(i); diff --git a/benchmarks/func_call_loop.py b/benchmarks/func_call_loop.py new file mode 100644 index 0000000..b49c64a --- /dev/null +++ b/benchmarks/func_call_loop.py @@ -0,0 +1,7 @@ +def add_one(x): + return x + 1 + +i = 0 +while i < 60000: + i = add_one(i) +print(i) diff --git a/benchmarks/func_call_loop.ynt b/benchmarks/func_call_loop.ynt new file mode 100644 index 0000000..4b93b0b --- /dev/null +++ b/benchmarks/func_call_loop.ynt @@ -0,0 +1,12 @@ +func add_one: + var x = %in + var y = ${x} + 1 calc + push_out ${y} +return + +var i = 0 +while ${i} < 60000: + call add_one with ${i} + var i = %out +end_while +print_line ${i} diff --git a/benchmarks/list_ops.js b/benchmarks/list_ops.js new file mode 100644 index 0000000..d2318a6 --- /dev/null +++ b/benchmarks/list_ops.js @@ -0,0 +1,9 @@ +const numbers = []; +for (let i = 0; i < 30000; i++) { + numbers.push(i); +} +let s = 0; +for (let i = 0; i < 30000; i++) { + s += numbers[i]; +} +console.log(s); diff --git a/benchmarks/list_ops.py b/benchmarks/list_ops.py new file mode 100644 index 0000000..6a95bb2 --- /dev/null +++ b/benchmarks/list_ops.py @@ -0,0 +1,7 @@ +numbers = [] +for i in range(30000): + numbers.append(i) +s = 0 +for i in range(30000): + s += numbers[i] +print(s) diff --git a/benchmarks/list_ops.ynt b/benchmarks/list_ops.ynt new file mode 100644 index 0000000..af52254 --- /dev/null +++ b/benchmarks/list_ops.ynt @@ -0,0 +1,15 @@ +list numbers new +var i = 0 +while ${i} < 30000: + list numbers add ${i} + var i = ${i} + 1 calc +end_while + +var i = 0 +var sum = 0 +while ${i} < 30000: + list numbers get ${i} + var sum = ${sum} + %out calc + var i = ${i} + 1 calc +end_while +print_line ${sum} diff --git a/benchmarks/run.ps1 b/benchmarks/run.ps1 new file mode 100644 index 0000000..c1138e5 --- /dev/null +++ b/benchmarks/run.ps1 @@ -0,0 +1,81 @@ +$ErrorActionPreference = 'Stop' +Set-Location $PSScriptRoot + +$benchmarks = @( + @{ Name='arith_loop'; YesNt='arith_loop.ynt'; Py='arith_loop.py'; Js='arith_loop.js' }, + @{ Name='func_call_loop'; YesNt='func_call_loop.ynt'; Py='func_call_loop.py'; Js='func_call_loop.js' }, + @{ Name='list_ops'; YesNt='list_ops.ynt'; Py='list_ops.py'; Js='list_ops.js' } +) + +$commands = @( + @{ Lang='YesNt'; Build={ param($b) "dotnet ../YesNt.Interpreter.App/bin/Release/net10.0/yesnt.dll $($b.YesNt)" } }, + @{ Lang='Python'; Build={ param($b) "python $($b.Py)" } }, + @{ Lang='Node'; Build={ param($b) "node $($b.Js)" } } +) + +function Invoke-Timed([string]$command) { + $sw = [System.Diagnostics.Stopwatch]::StartNew() + cmd /c $command > $null + $sw.Stop() + + if ($LASTEXITCODE -ne 0) { + throw "Command failed with exit code ${LASTEXITCODE}: $command" + } + + return [double]$sw.Elapsed.TotalMilliseconds +} + +$iterations = 8 +$results = @() + +foreach ($benchmark in $benchmarks) { + foreach ($command in $commands) { + $cmd = & $command.Build $benchmark + + [void](Invoke-Timed $cmd) # warm-up run + + $times = @() + for ($i = 0; $i -lt $iterations; $i++) { + $times += Invoke-Timed $cmd + } + + $sorted = $times | Sort-Object + $mean = ($times | Measure-Object -Average).Average + $median = if ($iterations % 2 -eq 0) { + ($sorted[$iterations / 2 - 1] + $sorted[$iterations / 2]) / 2 + } else { + $sorted[[int]($iterations / 2)] + } + + $results += [pscustomobject]@{ + Benchmark = $benchmark.Name + Language = $command.Lang + MeanMs = [math]::Round($mean, 2) + MedianMs = [math]::Round($median, 2) + MinMs = [math]::Round($sorted[0], 2) + MaxMs = [math]::Round($sorted[-1], 2) + } + } +} + +$results = $results | Sort-Object Benchmark, Language +$results | Format-Table -AutoSize + +Write-Host "`nRelative slowdown (lower baseline is better):" +$slowdowns = foreach ($name in ($results.Benchmark | Select-Object -Unique)) { + $group = $results | Where-Object Benchmark -eq $name + $yesnt = ($group | Where-Object Language -eq 'YesNt').MeanMs + $python = ($group | Where-Object Language -eq 'Python').MeanMs + $node = ($group | Where-Object Language -eq 'Node').MeanMs + + [pscustomobject]@{ + Benchmark = $name + YesNt_vs_Python_x = [math]::Round($yesnt / $python, 1) + YesNt_vs_Node_x = [math]::Round($yesnt / $node, 1) + } +} + +$slowdowns | Format-Table -AutoSize + +Write-Host "`nCSV:" +$results | ConvertTo-Csv -NoTypeInformation diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..548d867 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,47 @@ +# YesNt Documentation + +YesNt is a line-based, interpreted scripting language. +Each line is one statement. There are no multi-line expressions. + +## Guides + +| Document | Description | +| ------------------------------------------- | ---------------------------------------------------- | +| [Language Reference](language-reference.md) | Every statement, token, and operator in the language | +| [Library API](library-api.md) | How to embed the interpreter in a C# project | +| [Editor](editor.md) | How to use the terminal code editor | + +## Quick start + +### Running a script from the command line + +```bash +dotnet run --project YesNt.Interpreter.App -- path/to/script.ynt +``` + +### Hello world + +```ynt +print_line Hello, world! +``` + +### Variables and output + +```ynt +var name = Alice +print_line Hello, ${name}! +``` + +### Functions + +```ynt +func greet: + var msg = Hello, ${name}! + print_line ${msg} +return + +var name = Bob +call greet +``` + +Script files use the `.ynt` extension by convention. diff --git a/docs/editor.md b/docs/editor.md new file mode 100644 index 0000000..5daf983 --- /dev/null +++ b/docs/editor.md @@ -0,0 +1,52 @@ +# YesNt Code Editor + +`YesNt.CodeEditor` is a terminal editor for writing, formatting, running, and debugging YesNt scripts. + +## Start the editor + +```bash +dotnet run --project YesNt.CodeEditor -- [optional-path-to-file.ynt] +``` + +If you pass a file path, it is loaded on startup. + +## Modes + +- **Command mode:** enter editor commands in the `>>>` prompt. +- **Edit mode:** direct text editing with keyboard navigation. +- **Debug mode:** script output/debug information while running. + +## Command mode commands + +| Command | Description | +| --------------------- | ------------------------------------------------------------------- | +| `edit` | Switch to edit mode | +| `line ` | Jump to line `n` and switch to edit mode | +| `save [path]` | Save to current path or a new path | +| `load ` | Load a file | +| `new` | Create new file | +| `format` | Auto-format indentation | +| `run [path]` | Save then run script (runs unsaved buffer if no path is set) | +| `debug [path] [step]` | Save then run in debug mode (`step` enables step-by-step execution) | +| `exit` | Close the editor | + +## Edit mode controls + +- Arrow keys: move cursor +- Enter: split line +- Backspace/Delete: remove characters/merge lines +- **Alt+C:** return to command mode +- **Alt+T:** jump to top +- **Alt+B:** jump to bottom +- **Alt+S:** jump to start of line +- **Alt+E:** jump to end of line +- **Alt+R:** run script +- **Alt+D:** run debug mode +- **Alt+F:** format current file + +## Formatter behavior (quick summary) + +- Indents `func`, `if`, `else`, and `while` blocks. +- Dedents on `return`, `end_if`, and `end_while`. +- `exit` / `throw` / `error` close active non-function blocks for following lines. +- Comment lines (`# ...`) are kept unindented. diff --git a/docs/language-reference.md b/docs/language-reference.md new file mode 100644 index 0000000..ec1e05f --- /dev/null +++ b/docs/language-reference.md @@ -0,0 +1,815 @@ +# YesNt Language Reference + +YesNt is a line-based scripting language. Every non-empty, non-comment line is one statement. +Execution proceeds top-to-bottom unless a control-flow statement changes the line counter. + +--- + +## Table of contents + +1. [Basic rules](#basic-rules) +2. [Comments](#comments) +3. [String literals](#string-literals) +4. [Variables](#variables) +5. [Console I/O](#console-io) +6. [Arithmetic](#arithmetic) +7. [Conditions](#conditions) +8. [Control flow](#control-flow) +9. [Functions](#functions) +10. [Lists](#lists) +11. [Processing](#processing) +12. [System](#system) +13. [Predefined tokens](#predefined-tokens) +14. [Termination](#termination) + +--- + +## Basic rules + +- Scripts are plain text files with the `.ynt` extension. +- Each non-empty line is one statement. There are no multi-line expressions. +- Leading and trailing whitespace on each line is ignored. +- Lines starting with `#` are comments. Inline comments are not supported. Everything after the keyword is treated as its argument. +- `${variable}` anywhere in a line is replaced with the variable's value before the statement runs. +- Text wrapped in double quotes (`"..."`) is a string literal. Its contents are not matched as keywords and support escape sequences like `\n` and `\t`. + +## Comments + +```ynt +# This is a comment. +print_line Hello # inline comments are NOT supported. Everything after print_line is the argument +``` + +Only whole-line comments (lines whose first non-whitespace character is `#`) are supported. + +--- + +## String literals + +Double-quoted strings protect their content from keyword matching and allow escape sequences. + +```ynt +print_line "Hello, world!" +print_line "Line one\nLine two" +print_line "She said \"hi\"" +``` + +| Escape | Meaning | +| ------ | -------------------- | +| `\n` | Newline | +| `\r` | Carriage return | +| `\t` | Horizontal tab | +| `\b` | Backspace | +| `\f` | Form feed | +| `\a` | Alert (bell) | +| `\v` | Vertical tab | +| `\"` | Literal double-quote | +| `\\` | Literal backslash | + +Variable interpolation (`${name}`) is **not** evaluated inside string literals - the braces +and content are passed through verbatim. + +```ynt +var x = world +print_line "${x}" # prints the literal text: ${x} +print_line "hello " ${x} # prints: hello world (interpolation outside the literal) +``` + +--- + +## Variables + +### Local variables - `var` + +``` +var = +``` + +Defines or updates a variable scoped to the current function (or the top level if called outside a function). +The value is everything after `=`, trimmed. + +```ynt +var count = 0 +var greeting = Hello, world! +``` + +Variable names may only contain letters and digits (`[a-zA-Z0-9]`). + +### Global variables - `global` + +``` +global = +``` + +Defines or updates a variable that is visible across all function scopes and background tasks. + +```ynt +global total = 100 +``` + +### Reading a variable - `${name}` + +`${name}` is an inline token that is replaced with the variable's value before the statement executes. +It can appear anywhere in a line and multiple occurrences are replaced left to right. +Local variables are checked first; if not found, the global table is checked. + +```ynt +var a = 5 +var b = 10 +print_line ${a} plus ${b} +``` + +### Deleting a variable - `delete` + +``` +delete +``` + +Removes the variable. Local scope is checked first; if not found, the global table is used. +Raises an error if the variable does not exist in either scope. + +```ynt +var temp = scratch +delete temp +``` + +--- + +## Console I/O + +### Print with newline - `print_line` + +``` +print_line +print_line +``` + +Writes `` followed by a newline. With no argument, writes a blank line. + +```ynt +print_line Hello! +print_line +print_line Done. +``` + +### Print without newline - `print` + +``` +print +``` + +Writes `` without a trailing newline. + +```ynt +print Enter your name: +var name = %read_line +print_line Hello, ${name}! +``` + +### Read a line of input - `%read_line` + +`%read_line` is an inline token that is replaced with one line of text read from standard input. + +```ynt +var answer = %read_line +print_line You typed: ${answer} +``` + +### Read a single key - `%read_key` + +`%read_key` is an inline token that is replaced with the single character pressed by the user (no Enter required). + +```ynt +print Press any key... +var key = %read_key +print_line You pressed: ${key} +``` + +### Clear the console - `clear` + +``` +clear +``` + +Clears the console window. + +--- + +## Arithmetic + +Arithmetic is a **postfix** modifier applied at the end of a line with the `calc` keyword. + +``` + calc +``` + +Any numeric sub-expression matching the pattern `number op number [op number …]` is evaluated +and replaced with the result. Supported operators (highest to lowest precedence): + +| Operator | Operation | +| -------- | ----------------------------- | +| `(…)` | Parentheses (evaluated first) | +| `^` | Exponentiation | +| `%` | Modulo | +| `/` | Division | +| `*` | Multiplication | +| `-` | Subtraction | +| `+` | Addition (lowest precedence) | + +Adjacent sign characters (`++`, `--`, `-+`, `+-`) are normalised before evaluation. + +```ynt +var x = 3 +var y = 4 +var sum = ${x} + ${y} calc # 7 +var expr = 2 + 3 * 4 calc # 14 (* before +) +var parens = (2 + 3) * 4 calc # 20 +var power = 2 ^ 10 calc # 1024 +var remainder = 17 % 5 calc # 2 +``` + +--- + +## Conditions + +Conditions are used in `if` and `while` statements. A condition is a string of the form: + +``` + +``` + +| Operator | Meaning | +| -------- | --------------------------------------------- | +| `==` | Equal (string comparison, case-sensitive) | +| `!=` | Not equal (string comparison, case-sensitive) | +| `<` | Less than (numeric) | +| `>` | Greater than (numeric) | +| `<=` | Less than or equal (numeric) | +| `>=` | Greater than or equal (numeric) | + +Numeric comparisons (`<`, `>`, `<=`, `>=`) parse both sides with +culture-invariant decimal rules (`.` or `,` as decimal separator). + +A bare value of `True` or `False` (case-insensitive) is also a valid condition. + +```ynt +var x = 10 +if ${x} > 5: + print_line x is greater than 5 +end_if +``` + +--- + +## Control flow + +### If / else / end_if + +``` +if : + +else: + +end_if +``` + +`else:` is optional. `if` / `else:` / `end_if` blocks can be nested. + +```ynt +var score = 75 +if ${score} >= 60: + print_line Pass +else: + print_line Fail +end_if +``` + +### While loop + +``` +while : + +end_while +``` + +The condition is checked before each iteration. `while` / `end_while` blocks can be nested. + +```ynt +var i = 1 +while ${i} <= 5: + print_line ${i} + var i = ${i} + 1 calc +end_while +``` + +### Labels and goto + +``` +label : +goto +``` + +`label` marks a target. `goto` performs an unconditional jump to that label. +At the top level, labels are file-scoped, you can jump to any label in the file. Inside a function, labels are restricted to the current function; you cannot jump to a label outside the calling function. + +```ynt +label loop: + print_line tick + goto loop +``` + +### Conditional goto + +``` +if goto