mirror of
https://github.com/Stone-Red-Code/YesNt-Interpreter.git
synced 2026-09-04 00:56:31 +02:00
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -1,5 +1,57 @@
|
||||
# YesNt
|
||||

|
||||
|
||||
> YesNt is a simple imperative and interpreted language.
|
||||
- [Documentation](docs/README.md)
|
||||
- [Language Reference](docs/language-reference.md)
|
||||
- [Releases](https://github.com/Stone-Red-Code/YesNt-Interpreter/releases)
|
||||
|
||||
Check out the [Wiki](https://github.com/Stone-Red-Code/YesNt-Interpreter/wiki) (Work in progress)
|
||||
## 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}
|
||||
```
|
||||
|
||||
+116
@@ -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 = value` | `var x = value` | Local variable define/update. |
|
||||
| Variables | `!<x = value` | `global x = value` | Global variable define/update. |
|
||||
| Variables | `del x` | `delete x` | Deletes local first, then global. |
|
||||
| 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`.
|
||||
|
||||
+57
-1
@@ -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
|
||||
|
||||
+165
-38
@@ -1,40 +1,58 @@
|
||||
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
|
||||
{
|
||||
private readonly InputHandler inputHandler;
|
||||
private readonly SyntaxHighlighter syntaxHighlighter;
|
||||
private readonly List<string> debugOutput = new();
|
||||
private readonly List<string> debugOutput = [];
|
||||
|
||||
private readonly Point oldSize = new Point(0, 0);
|
||||
public YesNtInterpreter YesNtInterpreter { get; } = new();
|
||||
public int LineOffset { get; set; } = 0;
|
||||
public List<string> Lines { get; } = new();
|
||||
public List<string> 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()
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
Load(path);
|
||||
_ = Load(path);
|
||||
}
|
||||
}
|
||||
|
||||
public TextEditor()
|
||||
{
|
||||
YesNtInterpreter.Initialize();
|
||||
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) =>
|
||||
{
|
||||
if (SizeChanged() && EditMode != Mode.Debug)
|
||||
{
|
||||
Display(true);
|
||||
|
||||
if (EditMode == Mode.Command)
|
||||
{
|
||||
InputHandler.WriteStatus(string.Empty);
|
||||
Console.SetCursorPosition(3, Console.WindowHeight - 2);
|
||||
}
|
||||
}
|
||||
};
|
||||
timer.Start();
|
||||
}
|
||||
|
||||
public void Run()
|
||||
@@ -97,7 +115,7 @@ namespace YesNt.CodeEditor
|
||||
Console.SetCursorPosition(0, Console.WindowHeight - 2);
|
||||
Console.Write(">>>" + new string(' ', Console.WindowWidth - 3));
|
||||
|
||||
Console.SetCursorPosition(Math.Min(CursorPosition.X + GetSpacing() + 2, Console.WindowWidth - 1), CursorPosition.Y - LineOffset);
|
||||
Console.SetCursorPosition(Math.Min(CursorPosition.X + GetSpacing() + 2, Console.WindowWidth - 1), Math.Min(CursorPosition.Y - LineOffset, Console.WindowHeight - 4));
|
||||
|
||||
Console.CursorVisible = true;
|
||||
}
|
||||
@@ -131,14 +149,7 @@ namespace YesNt.CodeEditor
|
||||
|
||||
if (CurrentPath.Trim() != path.Trim() && loadIfExists)
|
||||
{
|
||||
if (Load(path))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return Load(path);
|
||||
}
|
||||
if (string.IsNullOrEmpty(Path.GetExtension(path)))
|
||||
{
|
||||
@@ -184,12 +195,137 @@ namespace YesNt.CodeEditor
|
||||
}
|
||||
}
|
||||
|
||||
public int GetSpacing()
|
||||
{
|
||||
int padding = (Console.WindowHeight + LineOffset - 3).ToString().Length;
|
||||
padding = Math.Max(padding, Lines.Count.ToString().Length);
|
||||
padding += 1;
|
||||
return padding;
|
||||
}
|
||||
|
||||
public void FormatLines()
|
||||
{
|
||||
const int indentationSize = 4;
|
||||
List<string> blockStack = [];
|
||||
|
||||
for (int i = 0; i < Lines.Count; i++)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string ToLiteral(string input)
|
||||
{
|
||||
return Microsoft.CodeAnalysis.CSharp.SymbolDisplay.FormatLiteral(input, false);
|
||||
}
|
||||
|
||||
private void YesNtInterpreter_OnDebugOutput(string output)
|
||||
{
|
||||
debugOutput.Add(output);
|
||||
}
|
||||
|
||||
private void YesNtInterpreter_OnLineExecuted(Interpreter.Runtime.DebugEventArgs e)
|
||||
private void YesNtInterpreter_OnLineExecuted(DebugEventArgs e)
|
||||
{
|
||||
lock (Console.Out)
|
||||
{
|
||||
@@ -202,11 +338,11 @@ namespace YesNt.CodeEditor
|
||||
|
||||
if (e.OriginalLine == e.CurrentLine)
|
||||
{
|
||||
Console.WriteLine($"{sharedString}[{e.CurrentLine}] ==>");
|
||||
Console.WriteLine($"{sharedString}[{ToLiteral(e.CurrentLine)}] ==>");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"{sharedString}[{e.OriginalLine}] => [{e.CurrentLine}] ==>");
|
||||
Console.WriteLine($"{sharedString}[{ToLiteral(e.OriginalLine)}] => [{ToLiteral(e.CurrentLine)}] ==>");
|
||||
}
|
||||
Console.ForegroundColor = ConsoleColor.Gray;
|
||||
}
|
||||
@@ -217,6 +353,14 @@ namespace YesNt.CodeEditor
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,16 +376,6 @@ namespace YesNt.CodeEditor
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -254,16 +388,10 @@ namespace YesNt.CodeEditor
|
||||
}
|
||||
}
|
||||
|
||||
internal class Point
|
||||
internal class Point(int x, int y)
|
||||
{
|
||||
public int X { get; set; }
|
||||
public int Y { get; set; }
|
||||
|
||||
public Point(int x, int y)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
}
|
||||
public int X { get; set; } = x;
|
||||
public int Y { get; set; } = y;
|
||||
}
|
||||
|
||||
internal enum Mode
|
||||
@@ -272,4 +400,3 @@ namespace YesNt.CodeEditor
|
||||
Command,
|
||||
Debug
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,17 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace YesNt.CodeEditor
|
||||
{
|
||||
internal class InputHandler
|
||||
{
|
||||
private readonly TextEditor textEditor;
|
||||
namespace YesNt.CodeEditor;
|
||||
|
||||
public InputHandler(TextEditor textEditor)
|
||||
internal class InputHandler(TextEditor textEditor)
|
||||
{
|
||||
this.textEditor = textEditor;
|
||||
}
|
||||
private readonly TextEditor textEditor = textEditor;
|
||||
|
||||
public bool HandleInput()
|
||||
{
|
||||
while (Console.KeyAvailable)
|
||||
{
|
||||
Console.ReadKey(true);
|
||||
_ = Console.ReadKey(true);
|
||||
}
|
||||
if (textEditor.EditMode == Mode.Edit)
|
||||
{
|
||||
@@ -50,14 +45,23 @@ namespace YesNt.CodeEditor
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -128,13 +132,18 @@ namespace YesNt.CodeEditor
|
||||
StringBuilder lineBuilder = new StringBuilder(textEditor.Lines[textEditor.CursorPosition.Y]);
|
||||
while (lineBuilder.Length <= textEditor.CursorPosition.X)
|
||||
{
|
||||
lineBuilder.Append(' ');
|
||||
_ = lineBuilder.Append(' ');
|
||||
}
|
||||
|
||||
textEditor.Lines[textEditor.CursorPosition.Y] = lineBuilder.ToString();
|
||||
|
||||
if (keyInfo.Key == ConsoleKey.Backspace)
|
||||
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)
|
||||
@@ -228,40 +237,22 @@ namespace YesNt.CodeEditor
|
||||
break;
|
||||
|
||||
case "save":
|
||||
textEditor.Save(input, false);
|
||||
_ = 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;
|
||||
}
|
||||
ExecuteWithDebugScreen(input, false, false);
|
||||
break;
|
||||
|
||||
case "debug":
|
||||
if (textEditor.Save(input, true))
|
||||
if (TryParseDebugCommand(input, out bool stepMode, out string parsedPath))
|
||||
{
|
||||
textEditor.EditMode = Mode.Debug;
|
||||
Console.Clear();
|
||||
Console.CursorVisible = true;
|
||||
textEditor.YesNtInterpreter.Execute(textEditor.CurrentPath, true);
|
||||
while (Console.KeyAvailable)
|
||||
{
|
||||
Console.ReadKey(true);
|
||||
string saveInput = string.IsNullOrWhiteSpace(parsedPath) ? "debug" : $"debug {parsedPath}";
|
||||
ExecuteWithDebugScreen(saveInput, true, stepMode);
|
||||
}
|
||||
Console.ReadKey();
|
||||
WriteStatus(string.Empty);
|
||||
textEditor.EditMode = Mode.Command;
|
||||
else
|
||||
{
|
||||
WriteStatus("Invalid arguments!");
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -278,7 +269,7 @@ namespace YesNt.CodeEditor
|
||||
|
||||
try
|
||||
{
|
||||
textEditor.Load(path);
|
||||
_ = textEditor.Load(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -299,6 +290,11 @@ namespace YesNt.CodeEditor
|
||||
WriteStatus(string.Empty);
|
||||
break;
|
||||
|
||||
case "format":
|
||||
textEditor.FormatLines();
|
||||
WriteStatus("Formatted!");
|
||||
break;
|
||||
|
||||
case "exit":
|
||||
return false;
|
||||
|
||||
@@ -316,5 +312,79 @@ namespace YesNt.CodeEditor
|
||||
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;
|
||||
}
|
||||
|
||||
for (int i = 1; i < parts.Length; i++)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,4 @@
|
||||
namespace YesNt.CodeEditor
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
private static void Main(string[] args)
|
||||
{
|
||||
TextEditor textEditor;
|
||||
|
||||
if (args.Length > 0)
|
||||
{
|
||||
textEditor = new TextEditor(args[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
textEditor = new TextEditor();
|
||||
}
|
||||
using YesNt.CodeEditor;
|
||||
|
||||
TextEditor textEditor = args.Length > 0 ? new TextEditor(args[0]) : new TextEditor();
|
||||
textEditor.Run();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"profiles": {
|
||||
"YesNt.CodeEditor": {
|
||||
"commandName": "Project"
|
||||
},
|
||||
"WSL": {
|
||||
"commandName": "WSL2",
|
||||
"environmentVariables": {},
|
||||
"distributionName": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,56 @@
|
||||
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
|
||||
{
|
||||
internal class SyntaxHighlighter
|
||||
{
|
||||
private readonly ReadOnlyCollection<StatementInformation> statementInformation;
|
||||
namespace YesNt.CodeEditor;
|
||||
|
||||
public SyntaxHighlighter(ReadOnlyCollection<StatementInformation> statementInformation)
|
||||
internal partial class SyntaxHighlighter(ReadOnlyCollection<StatementInformation> statementInformation)
|
||||
{
|
||||
this.statementInformation = statementInformation;
|
||||
private readonly ReadOnlyCollection<StatementInformation> statementInformation = statementInformation;
|
||||
private readonly string[] replacementValues = StringExtensions.ReplacementRules.Values.ToArray();
|
||||
|
||||
public static string Base64Encode(string plainText)
|
||||
{
|
||||
byte[] plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText);
|
||||
return System.Convert.ToBase64String(plainTextBytes);
|
||||
}
|
||||
|
||||
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.StartsWith('#'))
|
||||
if (input.TrimStart(' ').StartsWith('#'))
|
||||
{
|
||||
input = AddColorInformation(input, input, ConsoleColor.Gray, SearchMode.Exact);
|
||||
}
|
||||
else
|
||||
{
|
||||
MatchCollection matches = Regex.Matches(input, @"!!.");
|
||||
MatchCollection matches = StringRegex().Matches(input);
|
||||
for (int i = 0; i < matches.Count; i++)
|
||||
{
|
||||
input = AddColorInformation(input, matches[i].Value, Console.ForegroundColor, SearchMode.Contains);
|
||||
input = AddColorInformation(input, matches[i].Value, ConsoleColor.DarkYellow, SearchMode.Contains);
|
||||
}
|
||||
|
||||
matches = VariableRegex().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)
|
||||
@@ -45,76 +65,59 @@ namespace YesNt.CodeEditor
|
||||
SpaceAround.StartEnd => $" {statement.Name.Trim()} ",
|
||||
SpaceAround.Start => $" {statement.Name.Trim()}",
|
||||
SpaceAround.End => $"{statement.Name.Trim()} ",
|
||||
_ => statement.Name
|
||||
_ => statement.Name.Trim()
|
||||
};
|
||||
input = input.TrimEnd();
|
||||
if (statement.SearchMode == SearchMode.StartOfLine && input.StartsWith(name))
|
||||
input = input.TrimEnd(' ');
|
||||
string inputTrim = input.Trim(' ');
|
||||
if (statement.SearchMode == SearchMode.StartOfLine && inputTrim.StartsWith(name))
|
||||
{
|
||||
if (statement.Seperator is not null && input.Contains(statement.Seperator))
|
||||
if (statement.Separator is not null && input.Contains(statement.Separator))
|
||||
{
|
||||
input = AddColorInformation(input, statement.Seperator, statement.Color, SearchMode.StartOfLine);
|
||||
input = AddColorInformation(input, statement.Separator, statement.Color, SearchMode.StartOfLine);
|
||||
}
|
||||
else if (statement.Seperator is not null)
|
||||
else if (statement.Separator is not null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
input = AddColorInformation(input, input[..name.Length], statement.Color, statement.SearchMode);
|
||||
input = AddColorInformation(input, name, statement.Color, statement.SearchMode);
|
||||
}
|
||||
if (statement.SearchMode == SearchMode.Contains && $" {input} ".Contains(name))
|
||||
else if (statement.SearchMode == SearchMode.Contains && input.Contains(name))
|
||||
{
|
||||
if (statement.Seperator is not null && input.Contains(statement.Seperator))
|
||||
if (statement.Separator is not null && input.Contains(statement.Separator))
|
||||
{
|
||||
input = AddColorInformation(input, statement.Seperator, statement.Color, SearchMode.StartOfLine);
|
||||
input = AddColorInformation(input, statement.Separator, statement.Color, SearchMode.StartOfLine);
|
||||
}
|
||||
else if (statement.Seperator is not null)
|
||||
else if (statement.Separator is not null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
input = AddColorInformation($"{input} ", $"{input} ".Substring($"{input} ".IndexOf(name), name.Length), statement.Color, statement.SearchMode);
|
||||
input = AddColorInformation(input, input.Substring(input.IndexOf(name), name.Length), statement.Color, statement.SearchMode);
|
||||
}
|
||||
if (statement.SearchMode == SearchMode.EndOfLine && input.EndsWith(name))
|
||||
else if (statement.SearchMode == SearchMode.EndOfLine && input.EndsWith(name))
|
||||
{
|
||||
if (statement.Seperator is not null && input.Contains(statement.Seperator))
|
||||
if (statement.Separator is not null && input.Contains(statement.Separator))
|
||||
{
|
||||
input = AddColorInformation(input, statement.Seperator, statement.Color, SearchMode.StartOfLine);
|
||||
input = AddColorInformation(input, statement.Separator, statement.Color, SearchMode.StartOfLine);
|
||||
}
|
||||
else if (statement.Seperator is not null)
|
||||
else if (statement.Separator is not null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
input = AddColorInformation(input, input[^name.Length..], statement.Color, statement.SearchMode);
|
||||
}
|
||||
if (statement.SearchMode == SearchMode.Exact && input.Equals(name))
|
||||
else if (statement.SearchMode == SearchMode.Exact && inputTrim.Equals(name))
|
||||
{
|
||||
if (statement.Seperator is not null && input.Contains(statement.Seperator))
|
||||
if (statement.Separator is not null && input.Contains(statement.Separator))
|
||||
{
|
||||
input = AddColorInformation(input, statement.Seperator, statement.Color, SearchMode.StartOfLine);
|
||||
input = AddColorInformation(input, statement.Separator, statement.Color, SearchMode.StartOfLine);
|
||||
}
|
||||
else if (statement.Seperator is not null)
|
||||
else if (statement.Separator is not null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
input = AddColorInformation(input, input, statement.Color, statement.SearchMode);
|
||||
}
|
||||
}
|
||||
|
||||
matches = Regex.Matches(input, @">[a-zA-Z0-9]+");
|
||||
for (int i = 0; i < matches.Count; i++)
|
||||
{
|
||||
input = AddColorInformation(input, matches[i].Value, ConsoleColor.Cyan, SearchMode.Contains);
|
||||
}
|
||||
|
||||
matches = Regex.Matches(input, @"^<[a-zA-Z0-9]+");
|
||||
for (int i = 0; i < matches.Count; i++)
|
||||
{
|
||||
input = AddColorInformation(input, matches[i].Value, ConsoleColor.DarkCyan, SearchMode.StartOfLine);
|
||||
}
|
||||
|
||||
matches = Regex.Matches(input, @"^!<[a-zA-Z0-9]+");
|
||||
for (int i = 0; i < matches.Count; i++)
|
||||
{
|
||||
input = AddColorInformation(input, matches[i].Value, ConsoleColor.Blue, SearchMode.StartOfLine);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string part in input.Split("\0"))
|
||||
@@ -122,11 +125,11 @@ namespace YesNt.CodeEditor
|
||||
ConsoleColor consoleColor;
|
||||
string messagePart = part;
|
||||
|
||||
string stringColor = Regex.Match(messagePart, "(?<=(\\r))(.*)(?=\\r)").Value;
|
||||
string stringColor = StringColorRegex().Match(messagePart).Value;
|
||||
bool succ = int.TryParse(stringColor, out int colorIndex);
|
||||
if (succ && colorIndex >= 0 && colorIndex < 16)
|
||||
{
|
||||
messagePart = messagePart.Replace($"\r{stringColor}\r", string.Empty);
|
||||
messagePart = Base64Decode(messagePart.Replace("\x01" + stringColor + "\x01", string.Empty));
|
||||
consoleColor = (ConsoleColor)colorIndex;
|
||||
}
|
||||
else
|
||||
@@ -147,13 +150,27 @@ namespace YesNt.CodeEditor
|
||||
private static string AddColorInformation(string originalString, string value, ConsoleColor color, SearchMode searchMode)
|
||||
{
|
||||
int spacesAtEnd = value.WhiteSpaceAtEnd();
|
||||
string reult = searchMode switch
|
||||
|
||||
string base64Value = Base64Encode(value.TrimEnd());
|
||||
|
||||
string result = searchMode switch
|
||||
{
|
||||
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))
|
||||
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 reult;
|
||||
}
|
||||
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(@"(?<!\\)(?:\\\\{2})*""(?:\\.|[^""\\])*""")]
|
||||
private static partial Regex StringRegex();
|
||||
|
||||
// This regex matches the color information embedded in the string, which is in the format \x01{colorIndex}\x01{base64EncodedValue}.
|
||||
[GeneratedRegex(@"(?<=(\x01))(.*)(?=\x01)")]
|
||||
private static partial Regex StringColorRegex();
|
||||
}
|
||||
@@ -2,13 +2,20 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<PublishAot>True</PublishAot>
|
||||
<AssemblyName>yesntcode</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\.editorconfig" Link=".editorconfig" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YesNt.Interpreter\YesNt.Interpreter.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -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!");
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<OutputType>Exe</OutputType>
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<PublishAot>True</PublishAot>
|
||||
<AssemblyName>yesnt</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YesNt.Interpreter\YesNt.Interpreter.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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<MethodRegistration> statementMethods = [];
|
||||
List<MethodRegistration> 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<MethodRegistration> statementMethods,
|
||||
List<MethodRegistration> 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<MethodRegistration> statementMethods,
|
||||
List<MethodRegistration> 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<MethodRegistration> statementMethods,
|
||||
List<MethodRegistration> staticStatementMethods)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
_ = sb.AppendLine("// <auto-generated />");
|
||||
_ = 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<global::YesNt.Interpreter.Attributes.StatementAttribute, Action<string>> statements,");
|
||||
_ = sb.AppendLine(" out List<KeyValuePair<global::YesNt.Interpreter.Attributes.StaticStatementAttribute, Action>> staticStatements)");
|
||||
_ = sb.AppendLine(" {");
|
||||
|
||||
List<INamedTypeSymbol> allTypes = statementMethods
|
||||
.Concat(staticStatementMethods)
|
||||
.Select(x => x.ContainingType)
|
||||
.GroupBy(x => x, SymbolEqualityComparer.Default)
|
||||
.Select(g => g.First())
|
||||
.OrderBy(x => x.ToDisplayString())
|
||||
.ToList();
|
||||
|
||||
Dictionary<INamedTypeSymbol, string> instanceNames = new Dictionary<INamedTypeSymbol, string>(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<KeyValuePair<global::YesNt.Interpreter.Attributes.StatementAttribute, Action<string>>>();");
|
||||
|
||||
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<KeyValuePair<global::YesNt.Interpreter.Attributes.StaticStatementAttribute, Action>>();");
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsRoslynComponent>true</IsRoslynComponent>
|
||||
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="5.0.0" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<string> lines =
|
||||
[
|
||||
"unknown_command foo"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Invalid statement");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void RemoveStatementCausesInvalidStatementTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"sleep 100"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessageWithSetup(lines, "Invalid statement", interpreter =>
|
||||
{
|
||||
interpreter.RemoveStatement("sleep");
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DisabledStatementIsIgnoredTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"var x = hello",
|
||||
"${x}"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessageWithSetup(lines, "Variable \"x\" not found", interpreter =>
|
||||
{
|
||||
interpreter.DisableStatement("var");
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DisabledStatementCanBeReenabledTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"var result = overwritten",
|
||||
"${result}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqualWithSetup(lines, "overwritten", setup: interpreter =>
|
||||
{
|
||||
interpreter.DisableStatement("var");
|
||||
interpreter.EnableStatement("var");
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DisableNonExistentStatementIsNoOpTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"var x = ok",
|
||||
"${x}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqualWithSetup(lines, "ok", setup: interpreter =>
|
||||
{
|
||||
interpreter.DisableStatement("nonexistent_keyword");
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EnableNonDisabledStatementIsNoOpTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"var x = ok",
|
||||
"${x}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqualWithSetup(lines, "ok", setup: interpreter =>
|
||||
{
|
||||
interpreter.EnableStatement("var");
|
||||
});
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AddStatementWithHighPriorityRunsBeforeNormalTest()
|
||||
{
|
||||
List<string> 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<string> 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<string> 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<string> 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<string> 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.MethodLevel)]
|
||||
@@ -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<string> lines =
|
||||
[
|
||||
"var result = before",
|
||||
"exit",
|
||||
"var result = after",
|
||||
"${result}"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Planned termination by code");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void AbortAllStopsExecutionTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"abort_all",
|
||||
"var result = after",
|
||||
"${result}"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Canceling all tasks");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ThrowTerminatesWithErrorFlagTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"throw bad"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "with the message: bad");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ErrorTerminatesWithMessageTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"error soft"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "with the message: soft");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MissingLabelFailsTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"goto nowhere"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Label \"nowhere\" not found");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MissingFunctionFailsTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"call nowhere"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Function \"nowhere\" not found");
|
||||
}
|
||||
}
|
||||
@@ -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<string> lines =
|
||||
[
|
||||
"call yes",
|
||||
"func yes:",
|
||||
"global result = 1",
|
||||
"return",
|
||||
"${result}"
|
||||
];
|
||||
YesNtAssert.IsLastLineEqual(lines, "1");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LabelsTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"var result = 1",
|
||||
"goto yes",
|
||||
"var result = 0",
|
||||
"label yes:",
|
||||
"${result}"
|
||||
];
|
||||
YesNtAssert.IsLastLineEqual(lines, "1");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IfBlockTrueTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"var result = low",
|
||||
"if 6 > 5:",
|
||||
"var result = high",
|
||||
"end_if",
|
||||
"${result}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "high");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IfElseFalseBranchTest()
|
||||
{
|
||||
List<string> 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<string> 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<string> lines =
|
||||
[
|
||||
"var i = 3",
|
||||
"while ${i} > 0:",
|
||||
"var i = ${i} - 1 calc",
|
||||
"end_while",
|
||||
"${i}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "0");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void WhileSkipBodyWhenFalseTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"var i = 0",
|
||||
"while ${i} > 0:",
|
||||
"var i = 99",
|
||||
"end_while",
|
||||
"${i}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "0");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NestedWhileLoopTest()
|
||||
{
|
||||
List<string> 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<string> 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<string> lines =
|
||||
[
|
||||
"var result = 5",
|
||||
"if 1 == 2:",
|
||||
"var result = 1",
|
||||
"end_if",
|
||||
"${result}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "5");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void IfGotoTrueTest()
|
||||
{
|
||||
List<string> 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<string> lines =
|
||||
[
|
||||
"func set_result:",
|
||||
"global result = ok",
|
||||
"return",
|
||||
"if 1 == 1 call set_result",
|
||||
"${result}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "ok");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GotoInsideLoopExitsLoopTest()
|
||||
{
|
||||
List<string> 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<string> lines =
|
||||
[
|
||||
"label loop"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Invalid statement");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FunctionWithoutColonFailsTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"func missing"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Invalid statement");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MissingEndIfFailsTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"if 1 == 2:",
|
||||
"var result = 1"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "No matching end_if found");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ElseWithoutIfFailsTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"else:"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "No matching end_if found");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MissingEndWhileFailsTest()
|
||||
{
|
||||
List<string> 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<string> lines =
|
||||
[
|
||||
"end_while"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "No matching while found");
|
||||
}
|
||||
}
|
||||
@@ -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<string> lines = new List<string>()
|
||||
{
|
||||
"cal yes",
|
||||
"fnc yes",
|
||||
"!<result = 1",
|
||||
"ret",
|
||||
">result"
|
||||
};
|
||||
YesNtAssert.IsLastLineEqual(lines, "1");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LabelsTest()
|
||||
{
|
||||
List<string> lines = new List<string>()
|
||||
{
|
||||
"<result = 1",
|
||||
"jmp yes",
|
||||
"<result = 0",
|
||||
"lbl yes",
|
||||
">result"
|
||||
};
|
||||
YesNtAssert.IsLastLineEqual(lines, "1");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CalculationsTest()
|
||||
{
|
||||
Assert.Inconclusive();
|
||||
YesNtAssert.IsLineEqual("10 * 10 !calc", (20).ToString());
|
||||
}
|
||||
}
|
||||
@@ -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<string> lines =
|
||||
[
|
||||
"print_line hello"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsDebugOutput(lines, "hello");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void PrintWritesOutputTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"print hello"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsDebugOutput(lines, "hello");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ClearThrowsInNonInteractiveConsoleTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"clear"
|
||||
];
|
||||
|
||||
_ = Assert.Throws<IOException>(() => YesNtAssert.GetLastLine(lines));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReadLineReplacesTokenTest()
|
||||
{
|
||||
lock (ConsoleLock)
|
||||
{
|
||||
TextReader originalIn = Console.In;
|
||||
|
||||
try
|
||||
{
|
||||
Console.SetIn(new StringReader("typed value" + Environment.NewLine));
|
||||
|
||||
List<string> 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<string> 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");
|
||||
}
|
||||
}
|
||||
@@ -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<string> 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<string> 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<string> 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<string> lines =
|
||||
[
|
||||
"var x = %out"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "No out argument in stack");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void InParameterOutsideFunctionFailsTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"var x = %in"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Statement not allowed outside of function");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ReturnOutsideFunctionFailsTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"return"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Statement not allowed outside of function");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void PushOutOutsideFunctionFailsTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"push_out value"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Statement not allowed outside of function");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void FunctionWithoutColonFailsTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"func missing"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Invalid statement");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NestedFunctionDefinitionFailsTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"func outer:",
|
||||
"func inner:",
|
||||
"return"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Nested functions are not allowed");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LocalVariableDoesNotLeakToCallerTest()
|
||||
{
|
||||
List<string> 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<string> lines =
|
||||
[
|
||||
"clear_call_stack",
|
||||
"var result = ok",
|
||||
"${result}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "ok");
|
||||
}
|
||||
|
||||
// --- Error path tests ---
|
||||
|
||||
[TestMethod]
|
||||
public void AccessInWithoutArgFailsTest()
|
||||
{
|
||||
List<string> 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<string> 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<string> 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");
|
||||
}
|
||||
}
|
||||
@@ -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<string> 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<string> 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<string> 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<string> 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<string> lines =
|
||||
[
|
||||
"list items new",
|
||||
"list items delete",
|
||||
"list items length"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "List \"items\" not found");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ListMissingFailsTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"list missing get 0"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "List \"missing\" not found");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ListInvalidIndexFailsTest()
|
||||
{
|
||||
List<string> 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<string> lines =
|
||||
[
|
||||
"list items add"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Invalid statement");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ListScopeInsideFunctionTest()
|
||||
{
|
||||
List<string> 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<string> lines =
|
||||
[
|
||||
"list items new",
|
||||
"list items add \"hello world\"",
|
||||
"list items get 0",
|
||||
"var result = %out eval",
|
||||
"${result}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "hello world");
|
||||
}
|
||||
}
|
||||
@@ -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<string> 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<string> lines =
|
||||
[
|
||||
"%os"
|
||||
];
|
||||
|
||||
string? value = YesNtAssert.GetLastLine(lines);
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(value));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void CpuTokenProducesValueTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"%cpu"
|
||||
];
|
||||
|
||||
string? value = YesNtAssert.GetLastLine(lines);
|
||||
Assert.IsFalse(string.IsNullOrWhiteSpace(value));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Is64TokenProducesBooleanTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"%is64"
|
||||
];
|
||||
|
||||
string? value = YesNtAssert.GetLastLine(lines);
|
||||
Assert.AreEqual(Environment.Is64BitOperatingSystem.ToString(), value);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void PiTokenProducesPiTest()
|
||||
{
|
||||
List<string> 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<string> 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<string> 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 _));
|
||||
}
|
||||
}
|
||||
@@ -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<string> 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<string> lines =
|
||||
[
|
||||
"sleep 0",
|
||||
"var result = ok",
|
||||
"${result}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "ok");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SleepInvalidValueFailsTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"sleep nope"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "\"nope\" is not a valid time-out value");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void SleepRunsAndContinuesTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"sleep 5",
|
||||
"var result = ok",
|
||||
"${result}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "ok");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LengthPushesOutParameterTest()
|
||||
{
|
||||
List<string> 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<string> lines =
|
||||
[
|
||||
$"import {tempFile}",
|
||||
"${imported}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "yes");
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (File.Exists(tempFile))
|
||||
{
|
||||
File.Delete(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ImportMissingFileFailsTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
$"import {Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))}.ynt"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Could not find file");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TaskCanUpdateGlobalVariableTest()
|
||||
{
|
||||
List<string> 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<string> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<string> lines =
|
||||
[
|
||||
"var msg = \"hello world\"",
|
||||
"${msg}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "hello world");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringLiteralEscapesWorkTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"var msg = \"a\\n\\t\\\"b\"",
|
||||
"${msg}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "a\n\t\"b");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringLiteralPreventsVariableInterpolationTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"var x = hidden",
|
||||
"print_line \"${x}\""
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsDebugOutput(lines, "${x}");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void StringLiteralWorksWithListAddTest()
|
||||
{
|
||||
List<string> 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<string> lines =
|
||||
[
|
||||
"var x = \"\"",
|
||||
"length ${x}",
|
||||
"var len = %out",
|
||||
"${len}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "0");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EscapedBackslashProducesLiteralBackslashTest()
|
||||
{
|
||||
List<string> 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<string> 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<string> lines =
|
||||
[
|
||||
"var msg = \"hello"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Invalid string literal");
|
||||
}
|
||||
}
|
||||
@@ -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<string> lines =
|
||||
[
|
||||
"exec cmd with /c,echo yesnt",
|
||||
"var exitCode = %out",
|
||||
"${exitCode}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "0");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ExecWithInStackArgsRunsProcessTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"push_in /c",
|
||||
"push_in echo yesnt",
|
||||
"exec cmd",
|
||||
"var exitCode = %out",
|
||||
"${exitCode}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "0");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ExecInvalidProgramFailsTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"exec does_not_exist_abc_xyz"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Failed to start \"does_not_exist_abc_xyz\"");
|
||||
}
|
||||
}
|
||||
@@ -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<string> lines =
|
||||
[
|
||||
"var value = hi",
|
||||
"${value}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "hi");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void GlobalVariableReadTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"global value = hi",
|
||||
"${value}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "hi");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LocalVariableOverridesGlobalTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"global value = global",
|
||||
"var value = local",
|
||||
"${value}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "local");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DeleteLocalVariableTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"var value = a",
|
||||
"delete value",
|
||||
"global value = b",
|
||||
"${value}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "b");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DeleteMissingVariableFailsTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"delete missing"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Variable \"missing\" not found");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LetInvalidSyntaxFailsTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"var a"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Invalid statement");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LetInvalidNameFailsTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"var a b = 1"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Invalid syntax");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void OverwriteVariableTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"var x = first",
|
||||
"var x = second",
|
||||
"${x}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "second");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void EmptyStringVariableHasLengthZeroTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"var x = \"\"",
|
||||
"length ${x}",
|
||||
"var len = %out",
|
||||
"${len}"
|
||||
];
|
||||
|
||||
YesNtAssert.IsLastLineEqual(lines, "0");
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void MissingVariableReferenceFailsTest()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
"${missing}"
|
||||
];
|
||||
|
||||
YesNtAssert.ContainsTerminationMessage(lines, "Variable \"missing\" not found");
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="2.2.7" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="2.2.7" />
|
||||
<PackageReference Include="coverlet.collector" Version="3.1.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.3.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="4.1.0" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="4.1.0" />
|
||||
<PackageReference Include="coverlet.collector" Version="8.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -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<string> 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<string> lines = new List<string>()
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
line
|
||||
};
|
||||
];
|
||||
|
||||
(DebugEventArgs? debugEventArgs, _) = ExecuteAndCapture(lines, timeout);
|
||||
Assert.AreEqual(expected, debugEventArgs?.CurrentLine);
|
||||
}
|
||||
|
||||
public static void IsLineNotEqual(string line, string expected, int timeout = 1000)
|
||||
{
|
||||
List<string> lines =
|
||||
[
|
||||
line
|
||||
];
|
||||
|
||||
(DebugEventArgs? debugEventArgs, _) = ExecuteAndCapture(lines, timeout);
|
||||
Assert.AreNotEqual(expected, debugEventArgs?.CurrentLine);
|
||||
}
|
||||
|
||||
public static void ContainsTerminationMessage(List<string> lines, string expectedMessageFragment, int timeout = 1000)
|
||||
{
|
||||
(_, string debugOutput) = ExecuteAndCapture(lines, timeout);
|
||||
|
||||
StringAssert.Contains(debugOutput, expectedMessageFragment);
|
||||
}
|
||||
|
||||
public static void ContainsDebugOutput(List<string> lines, string expectedFragment, int timeout = 1000)
|
||||
{
|
||||
(_, string debugOutput) = ExecuteAndCapture(lines, timeout);
|
||||
|
||||
StringAssert.Contains(debugOutput, expectedFragment);
|
||||
}
|
||||
|
||||
public static string? GetLastLine(List<string> lines, int timeout = 1000)
|
||||
{
|
||||
(DebugEventArgs? debugEventArgs, _) = ExecuteAndCapture(lines, timeout);
|
||||
return debugEventArgs?.CurrentLine;
|
||||
}
|
||||
|
||||
public static void LastLineMatches(List<string> 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<string> lines, string expected, Action<YesNtInterpreter> setup, int timeout = 1000)
|
||||
{
|
||||
(DebugEventArgs? debugEventArgs, _) = ExecuteAndCapture(lines, timeout, setup);
|
||||
Assert.AreEqual(expected, debugEventArgs?.CurrentLine);
|
||||
}
|
||||
|
||||
public static void ContainsDebugOutputWithSetup(List<string> lines, string expectedFragment, Action<YesNtInterpreter> setup, int timeout = 1000)
|
||||
{
|
||||
(_, string debugOutput) = ExecuteAndCapture(lines, timeout, setup);
|
||||
StringAssert.Contains(debugOutput, expectedFragment);
|
||||
}
|
||||
|
||||
public static void ContainsTerminationMessageWithSetup(List<string> lines, string expectedMessageFragment, Action<YesNtInterpreter> setup, int timeout = 1000)
|
||||
{
|
||||
(_, string debugOutput) = ExecuteAndCapture(lines, timeout, setup);
|
||||
StringAssert.Contains(debugOutput, expectedMessageFragment);
|
||||
}
|
||||
|
||||
public static string? GetLastLineWithSetup(List<string> lines, Action<YesNtInterpreter> setup, int timeout = 1000)
|
||||
{
|
||||
(DebugEventArgs? debugEventArgs, _) = ExecuteAndCapture(lines, timeout, setup);
|
||||
return debugEventArgs?.CurrentLine;
|
||||
}
|
||||
|
||||
private static (DebugEventArgs? LastDebugEvent, string DebugOutput) ExecuteAndCapture(List<string> lines, int timeout, Action<YesNtInterpreter>? 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());
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,91 @@
|
||||
using System;
|
||||
using System;
|
||||
|
||||
using YesNt.Interpreter.Enums;
|
||||
|
||||
namespace YesNt.Interpreter.Attributes
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
internal class StatementAttribute : Attribute
|
||||
{
|
||||
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; }
|
||||
namespace YesNt.Interpreter.Attributes;
|
||||
|
||||
internal StatementAttribute(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor color)
|
||||
/// <summary>
|
||||
/// Marks a method as a YesNt statement handler.
|
||||
/// The interpreter matches source lines against the <see cref="Name"/> keyword according to
|
||||
/// <see cref="SearchMode"/> and <see cref="SpaceAround"/> rules, then invokes the decorated method
|
||||
/// with the remaining argument text.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Methods decorated with this attribute must be instance methods on a class that inherits
|
||||
/// <see cref="Runtime.StatementRuntimeInformation"/> and must accept a single <see cref="string"/> parameter.
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
public class StatementAttribute : Attribute
|
||||
{
|
||||
/// <summary>Gets the keyword that identifies this statement in source code.</summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>Gets where in the line the keyword is searched for.</summary>
|
||||
public SearchMode SearchMode { get; }
|
||||
|
||||
/// <summary>Gets which sides of the keyword must be padded with a space.</summary>
|
||||
public SpaceAround SpaceAround { get; }
|
||||
|
||||
/// <summary>Gets or sets the syntax-highlight color used by the code editor.</summary>
|
||||
public ConsoleColor Color { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the execution priority. Statements with a lower <see cref="Priority"/> value
|
||||
/// run before those with a higher value. Defaults to <see cref="Priority.Normal"/>.
|
||||
/// </summary>
|
||||
public Priority Priority { get; set; } = Priority.Normal;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see langword="false"/>.
|
||||
/// </summary>
|
||||
public bool ExecuteInSearchMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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 <see langword="false"/>.
|
||||
/// </summary>
|
||||
public bool KeepStatementInArgs { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this statement should be excluded from syntax highlighting.
|
||||
/// Set to <see langword="true"/> when no <see cref="Color"/> is provided.
|
||||
/// </summary>
|
||||
public bool IgnoreSyntaxHighlighting { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 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. <c>call</c> vs <c>call … with …</c>).
|
||||
/// </summary>
|
||||
public string Separator { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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").
|
||||
/// </summary>
|
||||
public string BlockPair { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
public bool IsBlockEnd { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether this statement is an intermediate part of a block
|
||||
/// (e.g., "else:" between "if" and "end_if").
|
||||
/// </summary>
|
||||
public bool IsBlockIntermediate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="StatementAttribute"/> with a syntax-highlight color.
|
||||
/// </summary>
|
||||
/// <param name="name">The keyword that identifies this statement.</param>
|
||||
/// <param name="searchMode">Where in the line the keyword is matched.</param>
|
||||
/// <param name="spaceAround">Which sides of the keyword require a surrounding space.</param>
|
||||
/// <param name="color">The color used for syntax highlighting in the code editor.</param>
|
||||
public StatementAttribute(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor color)
|
||||
{
|
||||
Name = name;
|
||||
SearchMode = searchMode;
|
||||
@@ -25,7 +93,14 @@ namespace YesNt.Interpreter.Attributes
|
||||
Color = color;
|
||||
}
|
||||
|
||||
internal StatementAttribute(string name, SearchMode searchMode, SpaceAround spaceAround)
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="StatementAttribute"/> without a syntax-highlight color.
|
||||
/// The statement will be excluded from syntax highlighting.
|
||||
/// </summary>
|
||||
/// <param name="name">The keyword that identifies this statement.</param>
|
||||
/// <param name="searchMode">Where in the line the keyword is matched.</param>
|
||||
/// <param name="spaceAround">Which sides of the keyword require a surrounding space.</param>
|
||||
public StatementAttribute(string name, SearchMode searchMode, SpaceAround spaceAround)
|
||||
{
|
||||
Name = name;
|
||||
SearchMode = searchMode;
|
||||
@@ -33,4 +108,3 @@ namespace YesNt.Interpreter.Attributes
|
||||
IgnoreSyntaxHighlighting = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,30 @@
|
||||
|
||||
using YesNt.Interpreter.Enums;
|
||||
|
||||
namespace YesNt.Interpreter.Attributes
|
||||
{
|
||||
namespace YesNt.Interpreter.Attributes;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Methods decorated with this attribute must be instance methods on a class that inherits
|
||||
/// <see cref="Runtime.StatementRuntimeInformation"/> and must have no parameters.
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
|
||||
internal class StaticStatementAttribute : Attribute
|
||||
public class StaticStatementAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// 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 <see langword="false"/>.
|
||||
/// </summary>
|
||||
public bool ExecuteInSearchMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the execution priority relative to other static statements.
|
||||
/// Defaults to <see cref="Priority.Normal"/>.
|
||||
/// </summary>
|
||||
public Priority Priority { get; set; } = Priority.Normal;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,28 @@
|
||||
namespace YesNt.Interpreter.Enums
|
||||
{
|
||||
internal enum Priority
|
||||
namespace YesNt.Interpreter.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Controls the execution order of statements. Lower values run first.
|
||||
/// </summary>
|
||||
public enum Priority
|
||||
{
|
||||
/// <summary>Runs before all other statements. Used for syntax pre-processing such as string literals.</summary>
|
||||
PreProcessing,
|
||||
|
||||
/// <summary>Runs very early. Used for inline substitutions such as variable reads and parameter pops.</summary>
|
||||
Highest,
|
||||
|
||||
/// <summary>Runs early.</summary>
|
||||
VeryHigh,
|
||||
|
||||
/// <summary>Runs above normal order.</summary>
|
||||
High,
|
||||
|
||||
/// <summary>Default execution order.</summary>
|
||||
Normal,
|
||||
|
||||
/// <summary>Runs below normal order.</summary>
|
||||
Low,
|
||||
|
||||
/// <summary>Runs last. Used for control-flow and variable definitions that depend on substitutions being complete.</summary>
|
||||
VeryLow
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,19 @@
|
||||
namespace YesNt.Interpreter.Enums
|
||||
{
|
||||
namespace YesNt.Interpreter.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Determines where in a source line the interpreter searches for a statement keyword.
|
||||
/// </summary>
|
||||
public enum SearchMode
|
||||
{
|
||||
/// <summary>The keyword must appear at the beginning of the line.</summary>
|
||||
StartOfLine,
|
||||
|
||||
/// <summary>The keyword must appear at the end of the line.</summary>
|
||||
EndOfLine,
|
||||
|
||||
/// <summary>The keyword may appear anywhere in the line.</summary>
|
||||
Contains,
|
||||
|
||||
/// <summary>The entire line must exactly match the keyword.</summary>
|
||||
Exact
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,19 @@
|
||||
namespace YesNt.Interpreter.Enums
|
||||
{
|
||||
namespace YesNt.Interpreter.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies which sides of a statement keyword must be surrounded by a space when matching.
|
||||
/// </summary>
|
||||
public enum SpaceAround
|
||||
{
|
||||
/// <summary>A space is required both before and after the keyword.</summary>
|
||||
StartEnd,
|
||||
|
||||
/// <summary>A space is required before the keyword only.</summary>
|
||||
Start,
|
||||
|
||||
/// <summary>A space is required after the keyword only.</summary>
|
||||
End,
|
||||
|
||||
/// <summary>No surrounding spaces are required.</summary>
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,33 @@
|
||||
using System;
|
||||
|
||||
namespace YesNt.Interpreter.Runtime
|
||||
{
|
||||
namespace YesNt.Interpreter.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Provides per-line execution data raised through <see cref="YesNtInterpreter.OnLineExecuted"/>.
|
||||
/// </summary>
|
||||
public class DebugEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>Gets the 1-based line number of the executed line within its source file.</summary>
|
||||
public int LineNumber { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the line content after all statement transformations have been applied
|
||||
/// (e.g. after variable substitution). May differ from <see cref="OriginalLine"/>.
|
||||
/// </summary>
|
||||
public string CurrentLine { get; internal set; }
|
||||
|
||||
/// <summary>Gets the raw line content as it appeared in the source file.</summary>
|
||||
public string OriginalLine { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the task identifier of the task that executed this line, or <c>0</c> if the line
|
||||
/// was executed on the main thread.
|
||||
/// </summary>
|
||||
public int TaskId { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this line was executed inside a background task
|
||||
/// (spawned with the <c>task</c> statement).
|
||||
/// </summary>
|
||||
public bool IsTask { get; internal set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
namespace YesNt.Interpreter.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Central repository of all exit/error message strings used by <see cref="RuntimeInformation.Exit"/>.
|
||||
/// Keeping messages here ensures consistency and makes them easy to find or localise.
|
||||
/// </summary>
|
||||
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}";
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace YesNt.Interpreter.Runtime
|
||||
{
|
||||
internal class FunctionScope
|
||||
{
|
||||
public int CallerLine { get; }
|
||||
public Dictionary<string, string> Variables { get; } = new();
|
||||
public Dictionary<string, int> Labels { get; } = new();
|
||||
public Stack<string> Arguemtns { get; }
|
||||
public Stack<string> Results { get; } = new();
|
||||
namespace YesNt.Interpreter.Runtime;
|
||||
|
||||
public FunctionScope(int callerLine, Stack<string> arguemtns)
|
||||
/// <summary>
|
||||
/// Represents one frame on the function call stack. Created when a <c>call</c> statement is
|
||||
/// executed and popped when the matching <c>return</c> is reached.
|
||||
/// </summary>
|
||||
internal class FunctionScope(int callerLine, Stack<string> arguments)
|
||||
{
|
||||
CallerLine = callerLine;
|
||||
Arguemtns = arguemtns;
|
||||
}
|
||||
}
|
||||
/// <summary>Gets the zero-based line index to return to after this function completes.</summary>
|
||||
public int CallerLine { get; } = callerLine;
|
||||
|
||||
/// <summary>Gets the local variable table for this function invocation.</summary>
|
||||
public Dictionary<string, string> Variables { get; } = [];
|
||||
|
||||
/// <summary>Gets the local list table for this function invocation.</summary>
|
||||
public Dictionary<string, List<string>> Lists { get; } = [];
|
||||
|
||||
/// <summary>Gets the local label table for this function invocation.</summary>
|
||||
public Dictionary<string, int> Labels { get; } = [];
|
||||
|
||||
/// <summary>Gets the stack of input arguments passed to this function via <c>push_in</c>.</summary>
|
||||
public Stack<string> Arguments { get; } = arguments;
|
||||
|
||||
/// <summary>Gets the stack of output values pushed via <c>push_out</c>, consumed by the caller via <c>%out</c>.</summary>
|
||||
public Stack<string> Results { get; } = new();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace YesNt.Interpreter.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Exposes the script runtime state accessible to custom statement handlers registered
|
||||
/// via <see cref="YesNtInterpreter.AddStatement"/>.
|
||||
/// </summary>
|
||||
public interface IStatementContext
|
||||
{
|
||||
/// <summary>Gets the local variable table for the current scope.</summary>
|
||||
Dictionary<string, string> Variables { get; }
|
||||
|
||||
/// <summary>Gets or sets the global variable table shared across all scopes.</summary>
|
||||
Dictionary<string, string> GlobalVariables { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the text of the line currently being processed.
|
||||
/// Inline-substitution handlers (e.g. <c>%read_line</c>) write their result here.</summary>
|
||||
string CurrentLine { get; set; }
|
||||
|
||||
/// <summary>Gets or sets the zero-based index of the next line to execute.
|
||||
/// Set this to implement control-flow jumps inside a custom statement.</summary>
|
||||
int LineNumber { get; set; }
|
||||
|
||||
/// <summary>Terminates execution with the given message.</summary>
|
||||
/// <param name="message">The message written to debug output.</param>
|
||||
/// <param name="isError">
|
||||
/// <see langword="true"/> to signal an error termination;
|
||||
/// <see langword="false"/> for a planned, non-error termination.
|
||||
/// </param>
|
||||
void Exit(string message, bool isError);
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
/// <summary>
|
||||
/// Represents a single source line together with its location metadata.
|
||||
/// </summary>
|
||||
internal class Line(string content, string fileName, int lineNumber)
|
||||
{
|
||||
/// <summary>Gets or sets the raw text content of the line.</summary>
|
||||
public string Content { get; set; } = content;
|
||||
|
||||
/// <summary>Gets or sets the name of the source file this line originated from.</summary>
|
||||
public string FileName { get; set; } = fileName;
|
||||
|
||||
/// <summary>Gets or sets the zero-based line index within <see cref="FileName"/>.</summary>
|
||||
public int LineNumber { get; set; } = lineNumber;
|
||||
}
|
||||
@@ -1,26 +1,38 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
using YesNt.Interpreter.Utilities;
|
||||
|
||||
namespace YesNt.Interpreter.Runtime
|
||||
namespace YesNt.Interpreter.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <c>task</c> statement owns its own
|
||||
/// <see cref="RuntimeInformation"/> whose <see cref="ParentRuntimeInformation"/> points back
|
||||
/// to the main execution context.
|
||||
/// </summary>
|
||||
internal sealed class RuntimeInformation : IStatementContext
|
||||
{
|
||||
internal class RuntimeInformation
|
||||
{
|
||||
private RuntimeInformation parentRuntimeInformation;
|
||||
public event Action<string> OnDebugOutput;
|
||||
|
||||
public event Action<DebugEventArgs> OnLineExecuted;
|
||||
|
||||
private event Action<string, bool> OnExit;
|
||||
|
||||
private static int internalTaskId = 0;
|
||||
private int taskId = 0;
|
||||
private readonly Dictionary<string, string> topVariables = [];
|
||||
private readonly Dictionary<string, List<string>> topLists = [];
|
||||
|
||||
private readonly Dictionary<string, string> topVariables = new();
|
||||
private readonly Dictionary<string, int> topLabels = new();
|
||||
|
||||
public Dictionary<string, string> GloablVariables { get; set; } = new();
|
||||
public Dictionary<string, int> Functions { get; } = new();
|
||||
public Dictionary<string, string> GlobalVariables { get; set; } = [];
|
||||
public Dictionary<string, int> Functions { get; } = [];
|
||||
public Dictionary<int, int> BlockBoundaries { get; } = [];
|
||||
internal Action PreScanLinesAction { get; set; }
|
||||
public Stack<FunctionScope> FunctionCallStack { get; } = new();
|
||||
public Stack<string> InParametersStack { get; } = new();
|
||||
public Stack<string> OutParametersStack { get; set; } = new();
|
||||
public List<Line> Lines { get; set; } = new();
|
||||
public List<Line> Lines { get; set; } = [];
|
||||
public string CurrentLine { get; set; } = string.Empty;
|
||||
public string SearchLabel { get; set; } = string.Empty;
|
||||
public string SearchFunction { get; set; } = string.Empty;
|
||||
@@ -28,9 +40,9 @@ namespace YesNt.Interpreter.Runtime
|
||||
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 string WorkingDirectory { get; set; } = string.Empty;
|
||||
public bool IsTask => ParentRuntimeInformation is not null;
|
||||
public int TaskId => IsTask ? taskId : 0;
|
||||
public int TaskId { get => IsTask ? field : 0; private set; } = 0;
|
||||
public bool InternalIsInFunction { get; set; }
|
||||
|
||||
public bool IsInFunction
|
||||
@@ -39,66 +51,27 @@ namespace YesNt.Interpreter.Runtime
|
||||
set => InternalIsInFunction = value;
|
||||
}
|
||||
|
||||
public Dictionary<string, string> Variables
|
||||
{
|
||||
get
|
||||
{
|
||||
if (FunctionCallStack.Count == 0)
|
||||
{
|
||||
return topVariables;
|
||||
}
|
||||
else
|
||||
{
|
||||
return FunctionCallStack.Peek().Variables;
|
||||
}
|
||||
}
|
||||
}
|
||||
public Dictionary<string, string> Variables => FunctionCallStack.Count == 0 ? topVariables : FunctionCallStack.Peek().Variables;
|
||||
public Dictionary<string, List<string>> Lists => FunctionCallStack.Count == 0 ? topLists : FunctionCallStack.Peek().Lists;
|
||||
|
||||
public Dictionary<string, int> Labels
|
||||
{
|
||||
get
|
||||
{
|
||||
if (FunctionCallStack.Count == 0)
|
||||
{
|
||||
return topLabels;
|
||||
}
|
||||
else
|
||||
{
|
||||
return FunctionCallStack.Peek().Labels;
|
||||
}
|
||||
}
|
||||
}
|
||||
public Dictionary<string, int> Labels { get => FunctionCallStack.Count == 0 ? field : FunctionCallStack.Peek().Labels; } = [];
|
||||
|
||||
public RuntimeInformation ParentRuntimeInformation
|
||||
{
|
||||
get => parentRuntimeInformation;
|
||||
get;
|
||||
set
|
||||
{
|
||||
parentRuntimeInformation = value;
|
||||
if (parentRuntimeInformation is not null)
|
||||
{
|
||||
parentRuntimeInformation.OnExit += ParentRuntimeInformation_OnExit;
|
||||
}
|
||||
field = value;
|
||||
field?.OnExit += ParentRuntimeInformation_OnExit;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSearching => !string.IsNullOrWhiteSpace(SearchLabel + SearchFunction) || IsInFunction && FunctionCallStack.Count == 0;
|
||||
public bool IsSearching => !string.IsNullOrWhiteSpace(SearchLabel + SearchFunction) || (IsInFunction && FunctionCallStack.Count == 0);
|
||||
public bool IsLocalSearch { get; set; }
|
||||
|
||||
private event Action<string, bool> OnExit;
|
||||
|
||||
public event Action<string> OnDebugOutput;
|
||||
|
||||
public event Action<DebugEventArgs> 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)
|
||||
if ((Stop && !forceWrite) || (ParentRuntimeInformation?.StopAllTasks == true && !forceWrite))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -107,22 +80,22 @@ namespace YesNt.Interpreter.Runtime
|
||||
{
|
||||
if (IsTask)
|
||||
{
|
||||
parentRuntimeInformation!.WriteLine(output.FromSaveString(), forceWrite);
|
||||
ParentRuntimeInformation!.WriteLine(output.FromSafeString(), forceWrite);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnDebugOutput?.Invoke(output.FromSaveString() + Environment.NewLine);
|
||||
OnDebugOutput?.Invoke(output.FromSafeString() + Environment.NewLine);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(output.FromSaveString());
|
||||
Console.WriteLine(output.FromSafeString());
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(string output, bool forceWrite = false)
|
||||
{
|
||||
if (Stop && !forceWrite || parentRuntimeInformation?.StopAllTasks == true && !forceWrite)
|
||||
if ((Stop && !forceWrite) || (ParentRuntimeInformation?.StopAllTasks == true && !forceWrite))
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -131,31 +104,47 @@ namespace YesNt.Interpreter.Runtime
|
||||
{
|
||||
if (IsTask)
|
||||
{
|
||||
parentRuntimeInformation!.Write(output.FromSaveString(), forceWrite);
|
||||
ParentRuntimeInformation!.Write(output.FromSafeString(), forceWrite);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnDebugOutput?.Invoke(output.FromSaveString());
|
||||
OnDebugOutput?.Invoke(output.FromSafeString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Write(output.FromSaveString());
|
||||
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;
|
||||
Line stackLine = (ParentRuntimeInformation?.Lines ?? Lines).ElementAt(stackLineNumber);
|
||||
List<Line> 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;
|
||||
}
|
||||
@@ -163,7 +152,7 @@ namespace YesNt.Interpreter.Runtime
|
||||
{
|
||||
StopAllTasks = true;
|
||||
OnExit?.Invoke(message, StopAllTasks);
|
||||
parentRuntimeInformation?.Exit("Terminated by child task", true);
|
||||
ParentRuntimeInformation?.Exit(ExitMessages.TerminatedByChildTask, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +160,7 @@ namespace YesNt.Interpreter.Runtime
|
||||
{
|
||||
if (IsTask)
|
||||
{
|
||||
parentRuntimeInformation.LineExecuted(debugEventArgs);
|
||||
ParentRuntimeInformation.LineExecuted(debugEventArgs);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -182,15 +171,19 @@ namespace YesNt.Interpreter.Runtime
|
||||
public void Reset()
|
||||
{
|
||||
topVariables.Clear();
|
||||
topLists.Clear();
|
||||
Lines.Clear();
|
||||
GloablVariables.Clear();
|
||||
GlobalVariables.Clear();
|
||||
Labels.Clear();
|
||||
Functions.Clear();
|
||||
BlockBoundaries.Clear();
|
||||
FunctionCallStack.Clear();
|
||||
InParametersStack.Clear();
|
||||
OutParametersStack.Clear();
|
||||
ParentRuntimeInformation = null;
|
||||
SearchLabel = string.Empty;
|
||||
SearchFunction = string.Empty;
|
||||
CurrentFilePath = string.Empty;
|
||||
WorkingDirectory = string.Empty;
|
||||
CurrentLine = string.Empty;
|
||||
Stop = false;
|
||||
StopAllTasks = false;
|
||||
@@ -198,10 +191,13 @@ namespace YesNt.Interpreter.Runtime
|
||||
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
|
||||
#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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
using YesNt.Interpreter.Attributes;
|
||||
|
||||
namespace YesNt.Interpreter.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Pre-calculated statement handler information for faster matching.
|
||||
/// </summary>
|
||||
internal record StatementHandler(StatementAttribute Attribute, Action<string> Handler, string FullName);
|
||||
@@ -2,15 +2,34 @@
|
||||
|
||||
using YesNt.Interpreter.Enums;
|
||||
|
||||
namespace YesNt.Interpreter.Runtime
|
||||
{
|
||||
namespace YesNt.Interpreter.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// A read-only snapshot of a registered statement's metadata, used for tooling such as
|
||||
/// syntax highlighters. Instances are obtained from <see cref="YesNtInterpreter.StatementInformation"/>.
|
||||
/// </summary>
|
||||
public class StatementInformation
|
||||
{
|
||||
/// <summary>Gets the keyword that identifies this statement in source code.</summary>
|
||||
public string Name { get; internal set; }
|
||||
|
||||
/// <summary>Gets where in the line the keyword is searched for.</summary>
|
||||
public SearchMode SearchMode { get; internal set; }
|
||||
|
||||
/// <summary>Gets which sides of the keyword must be padded with a space.</summary>
|
||||
public SpaceAround SpaceAround { get; internal set; }
|
||||
|
||||
/// <summary>Gets the syntax-highlight color for this statement.</summary>
|
||||
public ConsoleColor Color { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether this statement is excluded from syntax highlighting.
|
||||
/// </summary>
|
||||
public bool IgnoreSyntaxHighlighting { get; internal set; }
|
||||
public string Seperator { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optional sub-string that must be present in the line for this statement to match,
|
||||
/// or <see langword="null"/> if no separator is required.
|
||||
/// </summary>
|
||||
public string Separator { get; set; }
|
||||
}
|
||||
@@ -1,7 +1,24 @@
|
||||
namespace YesNt.Interpreter.Runtime
|
||||
{
|
||||
namespace YesNt.Interpreter.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for all classes that host statement handler methods.
|
||||
/// Subclasses declare methods decorated with <see cref="Attributes.StatementAttribute"/> or
|
||||
/// <see cref="Attributes.StaticStatementAttribute"/>; the source generator
|
||||
/// (<c>GeneratedStatementRegistry</c>) discovers these at compile time and wires them up.
|
||||
/// </summary>
|
||||
internal abstract class StatementRuntimeInformation
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the runtime state for the current execution context.
|
||||
/// Injected by the generated registry before any handler is invoked.
|
||||
/// </summary>
|
||||
public RuntimeInformation RuntimeInfo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Trims surrounding whitespace and a trailing colon from a block or function name.
|
||||
/// </summary>
|
||||
protected static string NormalizeBlockName(string value)
|
||||
{
|
||||
return value.Trim().TrimEnd(':').Trim();
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,48 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// The main entry point for executing YesNt scripts.
|
||||
/// </summary>
|
||||
public class YesNtInterpreter
|
||||
{
|
||||
private readonly RuntimeInformation runtimeInfo = new RuntimeInformation();
|
||||
private Dictionary<StatementAttribute, Action<string>> statements = new();
|
||||
private List<KeyValuePair<StaticStatementAttribute, Action>> staticStatements = new();
|
||||
/// <summary>
|
||||
/// Raised after each line is executed in debug mode. The argument is <see langword="null"/>
|
||||
/// when execution ends (either normally or due to an error), allowing callers to detect completion.
|
||||
/// </summary>
|
||||
public event Action<DebugEventArgs> OnLineExecuted;
|
||||
|
||||
/// <summary>
|
||||
/// Raised in debug mode whenever the script produces output (e.g. via <c>print_line</c>).
|
||||
/// In non-debug mode output is written directly to <see cref="Console"/>.
|
||||
/// </summary>
|
||||
public event Action<string> OnDebugOutput;
|
||||
|
||||
private readonly RuntimeInformation runtimeInfo = new RuntimeInformation();
|
||||
private Dictionary<StatementAttribute, Action<string>> statements;
|
||||
private List<StatementHandler> statementHandlers;
|
||||
private List<List<StatementHandler>> lineMatchingHandlers = [];
|
||||
private readonly List<KeyValuePair<StaticStatementAttribute, Action>> staticStatements;
|
||||
private readonly Dictionary<string, List<KeyValuePair<StatementAttribute, Action<string>>>> disabledStatements = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets a read-only snapshot of all currently registered statements.
|
||||
/// Useful for building syntax highlighters or documentation tools.
|
||||
/// </summary>
|
||||
public ReadOnlyCollection<StatementInformation> StatementInformation
|
||||
{
|
||||
get
|
||||
{
|
||||
List<StatementInformation> informations = statements.Select(s =>
|
||||
List<StatementInformation> information = statements.Select(s =>
|
||||
{
|
||||
return new StatementInformation()
|
||||
{
|
||||
@@ -30,74 +51,243 @@ namespace YesNt.Interpreter.Runtime
|
||||
SpaceAround = s.Key.SpaceAround,
|
||||
Color = s.Key.Color,
|
||||
IgnoreSyntaxHighlighting = s.Key.IgnoreSyntaxHighlighting,
|
||||
Seperator = s.Key.Seperator
|
||||
Separator = s.Key.Separator
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return new ReadOnlyCollection<StatementInformation>(informations);
|
||||
return new ReadOnlyCollection<StatementInformation>(information);
|
||||
}
|
||||
}
|
||||
|
||||
public event Action<DebugEventArgs> OnLineExecuted;
|
||||
|
||||
public event Action<string> OnDebugOutput;
|
||||
|
||||
public void Stop()
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="YesNtInterpreter"/> and registers all built-in statements.
|
||||
/// </summary>
|
||||
public YesNtInterpreter()
|
||||
{
|
||||
runtimeInfo.Exit("Terminated by external process", true);
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||
Type[] types = assembly.GetTypes();
|
||||
|
||||
IEnumerable<Type> statementRuntimeInfos = types.Where(t => t.IsSubclassOf(typeof(StatementRuntimeInformation)));
|
||||
|
||||
statements.Clear();
|
||||
|
||||
foreach (Type type in statementRuntimeInfos)
|
||||
{
|
||||
object statementInfo = Activator.CreateInstance(type);
|
||||
|
||||
MethodInfo[] methodInfos = statementInfo.GetType().GetMethods();
|
||||
|
||||
StatementRuntimeInformation statementRuntimeInfo = statementInfo as StatementRuntimeInformation;
|
||||
statementRuntimeInfo.RuntimeInfo = runtimeInfo;
|
||||
|
||||
foreach (MethodInfo methodInfo in methodInfos)
|
||||
{
|
||||
StatementAttribute statementAttribute = methodInfo.GetCustomAttribute<StatementAttribute>();
|
||||
if (statementAttribute is not null)
|
||||
{
|
||||
Action<string> method = methodInfo.CreateDelegate(typeof(Action<string>), statementInfo) as Action<string>;
|
||||
statements.Add(statementAttribute, method);
|
||||
}
|
||||
|
||||
StaticStatementAttribute staticStatementAttribute = methodInfo.GetCustomAttribute<StaticStatementAttribute>();
|
||||
if (staticStatementAttribute is not null)
|
||||
{
|
||||
Action method = methodInfo.CreateDelegate(typeof(Action), statementInfo) as Action;
|
||||
staticStatements.Add(new(staticStatementAttribute, method));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statements = statements.OrderBy(s => s.Key.Priority).ToDictionary(x => x.Key, x => x.Value);
|
||||
staticStatements = staticStatements.OrderBy(s => s.Key.Priority).ToList();
|
||||
GeneratedStatementRegistry.Register(runtimeInfo, out statements, out staticStatements);
|
||||
UpdateStatementHandlers();
|
||||
runtimeInfo.PreScanLinesAction = PreScanLines;
|
||||
|
||||
runtimeInfo.OnDebugOutput += (s) => OnDebugOutput?.Invoke(s);
|
||||
runtimeInfo.OnLineExecuted += (DebugEventArgs e) => OnLineExecuted?.Invoke(e);
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a custom statement using a pre-built <see cref="StatementAttribute"/>.
|
||||
/// 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 <b>add</b> a second handler rather than replacing
|
||||
/// the built-in. Use <see cref="RemoveStatement"/> first to replace a built-in keyword.
|
||||
/// The statement list is re-sorted by priority after insertion.
|
||||
/// </summary>
|
||||
/// <param name="attribute">The attribute describing the keyword, search mode, and priority.</param>
|
||||
/// <param name="handler">
|
||||
/// The delegate invoked when the statement matches. Receives the argument text
|
||||
/// (the part of the line after the keyword, unless <see cref="StatementAttribute.KeepStatementInArgs"/> is set).
|
||||
/// </param>
|
||||
public void AddStatement(StatementAttribute attribute, Action<string> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a custom statement using a pre-built <see cref="StatementAttribute"/>,
|
||||
/// with access to the script's <see cref="IStatementContext"/> (variables, line number, output, etc.).
|
||||
/// </summary>
|
||||
/// <param name="attribute">The attribute describing the keyword, search mode, and priority.</param>
|
||||
/// <param name="handler">
|
||||
/// The delegate invoked when the statement matches. Receives the argument text and the current
|
||||
/// <see cref="IStatementContext"/> for reading/writing script state.
|
||||
/// </param>
|
||||
public void AddStatement(StatementAttribute attribute, Action<string, IStatementContext> handler)
|
||||
{
|
||||
AddStatement(attribute, args => handler(args, runtimeInfo));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a simple custom statement with default settings.
|
||||
/// </summary>
|
||||
/// <param name="name">The keyword to match.</param>
|
||||
/// <param name="searchMode">Where in the line the keyword is searched for.</param>
|
||||
/// <param name="spaceAround">Which sides of the keyword must be padded with a space.</param>
|
||||
/// <param name="handler">The delegate invoked when the statement matches.</param>
|
||||
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action<string> handler)
|
||||
{
|
||||
AddStatement(new StatementAttribute(name, searchMode, spaceAround), handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a simple custom statement with default settings,
|
||||
/// with access to the script's <see cref="IStatementContext"/> (variables, line number, output, etc.).
|
||||
/// </summary>
|
||||
/// <param name="name">The keyword to match.</param>
|
||||
/// <param name="searchMode">Where in the line the keyword is searched for.</param>
|
||||
/// <param name="spaceAround">Which sides of the keyword must be padded with a space.</param>
|
||||
/// <param name="handler">
|
||||
/// The delegate invoked when the statement matches. Receives the argument text and the current
|
||||
/// <see cref="IStatementContext"/> for reading/writing script state.
|
||||
/// </param>
|
||||
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action<string, IStatementContext> handler)
|
||||
{
|
||||
AddStatement(new StatementAttribute(name, searchMode, spaceAround), handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a simple custom statement with a specific syntax-highlight color.
|
||||
/// </summary>
|
||||
/// <param name="name">The keyword to match.</param>
|
||||
/// <param name="searchMode">Where in the line the keyword is searched for.</param>
|
||||
/// <param name="spaceAround">Which sides of the keyword must be padded with a space.</param>
|
||||
/// <param name="consoleColor">The color used for syntax highlighting.</param>
|
||||
/// <param name="handler">The delegate invoked when the statement matches.</param>
|
||||
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor consoleColor, Action<string> handler)
|
||||
{
|
||||
AddStatement(new StatementAttribute(name, searchMode, spaceAround, consoleColor), handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a simple custom statement with a specific syntax-highlight color,
|
||||
/// with access to the script's <see cref="IStatementContext"/> (variables, line number, output, etc.).
|
||||
/// </summary>
|
||||
/// <param name="name">The keyword to match.</param>
|
||||
/// <param name="searchMode">Where in the line the keyword is searched for.</param>
|
||||
/// <param name="spaceAround">Which sides of the keyword must be padded with a space.</param>
|
||||
/// <param name="consoleColor">The color used for syntax highlighting.</param>
|
||||
/// <param name="handler">
|
||||
/// The delegate invoked when the statement matches. Receives the argument text and the current
|
||||
/// <see cref="IStatementContext"/> for reading/writing script state.
|
||||
/// </param>
|
||||
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor consoleColor, Action<string, IStatementContext> handler)
|
||||
{
|
||||
AddStatement(new StatementAttribute(name, searchMode, spaceAround, consoleColor), handler);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters all handlers matching the specified keyword <paramref name="name"/>.
|
||||
/// </summary>
|
||||
/// <param name="name">The keyword to remove.</param>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disables all statements matching <paramref name="name"/> 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 <see cref="EnableStatement"/> to restore original behavior.
|
||||
/// </summary>
|
||||
/// <param name="name">The keyword of the statement(s) to disable.</param>
|
||||
public void DisableStatement(string name)
|
||||
{
|
||||
if (disabledStatements.ContainsKey(name))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<KeyValuePair<StatementAttribute, Action<string>>> matching =
|
||||
statements.Where(kv => kv.Key.Name == name).ToList();
|
||||
|
||||
if (matching.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
disabledStatements[name] = matching;
|
||||
|
||||
foreach (KeyValuePair<StatementAttribute, Action<string>> kv in matching)
|
||||
{
|
||||
statements[kv.Key] = _ => { };
|
||||
}
|
||||
UpdateStatementHandlers();
|
||||
PreScanLines();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Re-enables statements previously disabled with <see cref="DisableStatement"/>,
|
||||
/// restoring their original handlers.
|
||||
/// Has no effect if the statement is not currently disabled.
|
||||
/// </summary>
|
||||
/// <param name="name">The keyword of the statement(s) to re-enable.</param>
|
||||
public void EnableStatement(string name)
|
||||
{
|
||||
if (!disabledStatements.TryGetValue(name, out List<KeyValuePair<StatementAttribute, Action<string>>> saved))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<StatementAttribute, Action<string>> kv in saved)
|
||||
{
|
||||
statements[kv.Key] = kv.Value;
|
||||
}
|
||||
|
||||
_ = disabledStatements.Remove(name);
|
||||
UpdateStatementHandlers();
|
||||
PreScanLines();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests a graceful stop of the currently executing script.
|
||||
/// The interpreter will terminate at the next line boundary.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
runtimeInfo.Exit(ExitMessages.TerminatedByExternalProcess, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a YesNt script file.
|
||||
/// </summary>
|
||||
/// <param name="path">The path to the <c>.ynt</c> script file.</param>
|
||||
/// <param name="isDebugMode">
|
||||
/// When <see langword="true"/>, output is routed through <see cref="OnDebugOutput"/> instead of
|
||||
/// <see cref="Console"/> and line-execution events are raised via <see cref="OnLineExecuted"/>.
|
||||
/// </param>
|
||||
public void Execute(string path, bool isDebugMode = false)
|
||||
{
|
||||
runtimeInfo.Reset();
|
||||
runtimeInfo.IsDebugMode = isDebugMode;
|
||||
LoadFile(path);
|
||||
if (LoadFile(path))
|
||||
{
|
||||
Execute();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes a YesNt script supplied as an in-memory list of lines.
|
||||
/// </summary>
|
||||
/// <param name="lines">The script lines to execute.</param>
|
||||
/// <param name="isDebugMode">
|
||||
/// When <see langword="true"/>, output is routed through <see cref="OnDebugOutput"/> and
|
||||
/// line-execution events are raised via <see cref="OnLineExecuted"/>.
|
||||
/// </param>
|
||||
public void Execute(List<string> lines, bool isDebugMode = false)
|
||||
{
|
||||
runtimeInfo.Reset();
|
||||
@@ -105,25 +295,28 @@ namespace YesNt.Interpreter.Runtime
|
||||
|
||||
for (int i = 0; i < lines.Count; i++)
|
||||
{
|
||||
runtimeInfo.Lines.Add(new Line(lines[i], Path.GetFileName("#Memory#"), 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<Line> lines, Dictionary<string, string> gloablVariables, int startLine, RuntimeInformation parentRuntimeInformation)
|
||||
internal void Execute(List<Line> lines, Dictionary<string, string> globalVariables, int startLine, RuntimeInformation parentRuntimeInformation)
|
||||
{
|
||||
runtimeInfo.Reset();
|
||||
runtimeInfo.IsDebugMode = parentRuntimeInformation.IsDebugMode;
|
||||
runtimeInfo.Lines = lines;
|
||||
runtimeInfo.LineNumber = startLine;
|
||||
runtimeInfo.ParentRuntimeInformation = parentRuntimeInformation;
|
||||
runtimeInfo.GloablVariables = gloablVariables;
|
||||
runtimeInfo.GlobalVariables = globalVariables;
|
||||
if (parentRuntimeInformation.StopAllTasks)
|
||||
{
|
||||
runtimeInfo.Exit($"Parent task was terminated!", parentRuntimeInformation.StopAllTasks);
|
||||
runtimeInfo.Exit(ExitMessages.TerminatedByParentTask, parentRuntimeInformation.StopAllTasks);
|
||||
return;
|
||||
}
|
||||
PreScanLines();
|
||||
Execute();
|
||||
}
|
||||
|
||||
@@ -136,20 +329,25 @@ namespace YesNt.Interpreter.Runtime
|
||||
break;
|
||||
}
|
||||
|
||||
runtimeInfo.CurrentLine = runtimeInfo.Lines[runtimeInfo.LineNumber].Content.TrimEnd().Replace("\r", string.Empty);
|
||||
Line lineObj = runtimeInfo.Lines[runtimeInfo.LineNumber];
|
||||
runtimeInfo.CurrentLine = lineObj.Content;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(runtimeInfo.CurrentLine) || runtimeInfo.CurrentLine.StartsWith('#'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DebugEventArgs debugEventArgs = new DebugEventArgs()
|
||||
DebugEventArgs debugEventArgs = null;
|
||||
if (runtimeInfo.IsDebugMode)
|
||||
{
|
||||
debugEventArgs = new DebugEventArgs()
|
||||
{
|
||||
LineNumber = runtimeInfo.LineNumber + 1,
|
||||
OriginalLine = runtimeInfo.CurrentLine.FromSaveString(),
|
||||
OriginalLine = runtimeInfo.CurrentLine.FromSafeString(),
|
||||
IsTask = runtimeInfo.IsTask,
|
||||
TaskId = runtimeInfo.TaskId
|
||||
};
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<StaticStatementAttribute, Action> staticStatement in staticStatements)
|
||||
{
|
||||
@@ -165,9 +363,11 @@ namespace YesNt.Interpreter.Runtime
|
||||
bool statementFound = false;
|
||||
bool notSearchingLabel = !runtimeInfo.IsSearching;
|
||||
|
||||
foreach (KeyValuePair<StatementAttribute, Action<string>> statement in statements)
|
||||
List<StatementHandler> handlers = (runtimeInfo.LineNumber < lineMatchingHandlers.Count) ? lineMatchingHandlers[runtimeInfo.LineNumber] : [];
|
||||
|
||||
foreach (StatementHandler handler in handlers)
|
||||
{
|
||||
StatementAttribute statementAttribute = statement.Key;
|
||||
StatementAttribute statementAttribute = handler.Attribute;
|
||||
|
||||
if (!statementAttribute.ExecuteInSearchMode && runtimeInfo.IsSearching)
|
||||
{
|
||||
@@ -180,49 +380,31 @@ namespace YesNt.Interpreter.Runtime
|
||||
break;
|
||||
}
|
||||
|
||||
string name = statementAttribute.SpaceAround switch
|
||||
{
|
||||
SpaceAround.StartEnd => $" {statementAttribute.Name.Trim()} ",
|
||||
SpaceAround.Start => $" {statementAttribute.Name.Trim()}",
|
||||
SpaceAround.End => $"{statementAttribute.Name.Trim()} ",
|
||||
_ => statementAttribute.Name
|
||||
};
|
||||
string name = handler.FullName;
|
||||
|
||||
if (statementAttribute.Seperator is null || runtimeInfo.CurrentLine.Contains(statementAttribute.Seperator))
|
||||
if (statementAttribute.Separator is null || runtimeInfo.CurrentLine.Contains(statementAttribute.Separator, StringComparison.Ordinal))
|
||||
{
|
||||
if (statementAttribute.SearchMode == SearchMode.StartOfLine && runtimeInfo.CurrentLine.StartsWith(name))
|
||||
if (statementAttribute.SearchMode == SearchMode.StartOfLine && runtimeInfo.CurrentLine.StartsWith(name, StringComparison.Ordinal))
|
||||
{
|
||||
string copyLine = statementAttribute.KeepStatementInArgs ? runtimeInfo.CurrentLine : runtimeInfo.CurrentLine.Remove(0, name.Length);
|
||||
statement.Value.Invoke(copyLine);
|
||||
string copyLine = statementAttribute.KeepStatementInArgs ? runtimeInfo.CurrentLine : runtimeInfo.CurrentLine[name.Length..];
|
||||
handler.Handler.Invoke(copyLine);
|
||||
statementFound = true;
|
||||
}
|
||||
else if (statementAttribute.SearchMode == SearchMode.Contains && $" {runtimeInfo.CurrentLine} ".Contains(name))
|
||||
else if (statementAttribute.SearchMode == SearchMode.Contains && runtimeInfo.CurrentLine.Contains(name, StringComparison.Ordinal))
|
||||
{
|
||||
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);
|
||||
handler.Handler.Invoke(copyLine);
|
||||
statementFound = true;
|
||||
}
|
||||
else if (statementAttribute.SearchMode == SearchMode.Exact && runtimeInfo.CurrentLine.Equals(name))
|
||||
else if (statementAttribute.SearchMode == SearchMode.EndOfLine && runtimeInfo.CurrentLine.EndsWith(name, StringComparison.Ordinal))
|
||||
{
|
||||
statement.Value.Invoke(runtimeInfo.CurrentLine);
|
||||
string copyLine = statementAttribute.KeepStatementInArgs ? runtimeInfo.CurrentLine : runtimeInfo.CurrentLine[..^name.Length];
|
||||
handler.Handler.Invoke(copyLine);
|
||||
statementFound = true;
|
||||
}
|
||||
else if (statementAttribute.SearchMode == SearchMode.Exact && runtimeInfo.CurrentLine.Equals(name, StringComparison.Ordinal))
|
||||
{
|
||||
handler.Handler.Invoke(runtimeInfo.CurrentLine);
|
||||
statementFound = true;
|
||||
}
|
||||
}
|
||||
@@ -230,11 +412,11 @@ namespace YesNt.Interpreter.Runtime
|
||||
|
||||
if (!statementFound)
|
||||
{
|
||||
runtimeInfo.Exit("Invalid statement", true);
|
||||
runtimeInfo.Exit(ExitMessages.InvalidStatement, true);
|
||||
}
|
||||
if (runtimeInfo.IsDebugMode && notSearchingLabel)
|
||||
if (runtimeInfo.IsDebugMode && notSearchingLabel && debugEventArgs != null)
|
||||
{
|
||||
debugEventArgs.CurrentLine = runtimeInfo.CurrentLine.FromSaveString();
|
||||
debugEventArgs.CurrentLine = runtimeInfo.CurrentLine.FromSafeString();
|
||||
runtimeInfo.LineExecuted(debugEventArgs);
|
||||
}
|
||||
}
|
||||
@@ -243,15 +425,15 @@ namespace YesNt.Interpreter.Runtime
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(runtimeInfo.SearchLabel))
|
||||
{
|
||||
runtimeInfo.Exit($"Label \"{runtimeInfo.SearchLabel}\" not found", true);
|
||||
runtimeInfo.Exit(ExitMessages.LabelNotFound(runtimeInfo.SearchLabel), true);
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(runtimeInfo.SearchFunction))
|
||||
{
|
||||
runtimeInfo.Exit($"Function \"{runtimeInfo.SearchFunction}\" not found", true);
|
||||
runtimeInfo.Exit(ExitMessages.FunctionNotFound(runtimeInfo.SearchFunction), true);
|
||||
}
|
||||
else
|
||||
{
|
||||
runtimeInfo.Exit("End of file", false);
|
||||
runtimeInfo.Exit(ExitMessages.EndOfFile, false);
|
||||
}
|
||||
|
||||
if (runtimeInfo.IsDebugMode)
|
||||
@@ -261,20 +443,107 @@ namespace YesNt.Interpreter.Runtime
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadFile(string path)
|
||||
private bool LoadFile(string path)
|
||||
{
|
||||
path = Path.GetFullPath(path);
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
runtimeInfo.Exit($"File \"{path}\" not found!", true);
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
string[] lines = File.ReadAllLines(path);
|
||||
|
||||
runtimeInfo.WorkingDirectory = Path.GetDirectoryName(path);
|
||||
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
runtimeInfo.Lines.Add(new Line(lines[i], Path.GetFileName(path), 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<List<StatementHandler>>(runtimeInfo.Lines.Count);
|
||||
|
||||
// Dictionary to track open blocks by their expected end statement name
|
||||
Dictionary<string, Stack<int>> openBlocks = [];
|
||||
|
||||
for (int i = 0; i < runtimeInfo.Lines.Count; i++)
|
||||
{
|
||||
string content = runtimeInfo.Lines[i].Content;
|
||||
List<StatementHandler> 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<int> stack))
|
||||
{
|
||||
stack = new Stack<int>();
|
||||
openBlocks[blockPair] = stack;
|
||||
}
|
||||
stack.Push(i);
|
||||
}
|
||||
|
||||
// Track block ends
|
||||
if (handler.Attribute.IsBlockEnd && openBlocks.TryGetValue(handler.Attribute.Name, out Stack<int> 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<int> stack))
|
||||
{
|
||||
stack = new Stack<int>();
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,23 @@
|
||||
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
|
||||
{
|
||||
[Statement("jmp", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow)]
|
||||
[Statement("goto", 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))
|
||||
if (RuntimeInfo.Labels.TryGetValue(key, out int value))
|
||||
{
|
||||
RuntimeInfo.LineNumber = RuntimeInfo.Labels[key];
|
||||
RuntimeInfo.LineNumber = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -27,24 +26,24 @@ namespace YesNt.Interpreter.Statements
|
||||
}
|
||||
}
|
||||
|
||||
[Statement("jif", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow, Seperator = "|")]
|
||||
[Statement("if", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow, Separator = " goto ")]
|
||||
public void JumpIf(string args)
|
||||
{
|
||||
string[] parts = args.Split('|');
|
||||
string[] parts = args.Split(" goto ", 2, StringSplitOptions.None);
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
RuntimeInfo.Exit("Invalid syntax", true);
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
|
||||
return;
|
||||
}
|
||||
|
||||
string key = parts[0].Trim();
|
||||
string condition = parts[1].Trim();
|
||||
string condition = parts[0].Trim();
|
||||
string key = NormalizeBlockName(parts[1]);
|
||||
|
||||
bool? result = Evaluator.EvaluateCondition(condition);
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
RuntimeInfo.Exit("Invalid operation", true);
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidOperation, true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,9 +52,9 @@ namespace YesNt.Interpreter.Statements
|
||||
return;
|
||||
}
|
||||
|
||||
if (RuntimeInfo.Labels.ContainsKey(key))
|
||||
if (RuntimeInfo.Labels.TryGetValue(key, out int value))
|
||||
{
|
||||
RuntimeInfo.LineNumber = RuntimeInfo.Labels[key];
|
||||
RuntimeInfo.LineNumber = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -64,18 +63,24 @@ namespace YesNt.Interpreter.Statements
|
||||
}
|
||||
}
|
||||
|
||||
[Statement("lbl", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, ExecuteInSearchMode = true)]
|
||||
[Statement("label", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, ExecuteInSearchMode = true, Separator = ":")]
|
||||
public void FindLabel(string args)
|
||||
{
|
||||
string key = args.Trim();
|
||||
if (RuntimeInfo.Labels.ContainsKey(key))
|
||||
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;
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.Labels.Add(key, RuntimeInfo.LineNumber);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(RuntimeInfo.SearchLabel) && RuntimeInfo.SearchLabel == key)
|
||||
{
|
||||
@@ -84,115 +89,195 @@ namespace YesNt.Interpreter.Statements
|
||||
}
|
||||
}
|
||||
|
||||
[Statement("cal", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, Priority = Priority.VeryLow)]
|
||||
[Statement("call", 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<string>(RuntimeInfo.InParametersStack.Reverse())));
|
||||
RuntimeInfo.InParametersStack.Clear();
|
||||
|
||||
if (RuntimeInfo.Functions.ContainsKey(key))
|
||||
{
|
||||
RuntimeInfo.LineNumber = RuntimeInfo.Functions[key];
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.SearchFunction = key;
|
||||
}
|
||||
CallFunction(NormalizeBlockName(args));
|
||||
}
|
||||
|
||||
[Statement("cif", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, Priority = Priority.VeryLow, Seperator = "|")]
|
||||
[Statement("if", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, Priority = Priority.VeryLow, Separator = " call ")]
|
||||
public void CallIf(string args)
|
||||
{
|
||||
string[] parts = args.Split('|');
|
||||
string[] parts = args.Split(" call ", 2, StringSplitOptions.None);
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
RuntimeInfo.Exit("Invalid syntax", true);
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
|
||||
return;
|
||||
}
|
||||
|
||||
string key = parts[0].Trim();
|
||||
string condition = parts[1].Trim();
|
||||
|
||||
string condition = parts[0].Trim();
|
||||
bool? result = Evaluator.EvaluateCondition(condition);
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
RuntimeInfo.Exit("Invalid operation", true);
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidOperation, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == false)
|
||||
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;
|
||||
}
|
||||
|
||||
RuntimeInfo.FunctionCallStack.Push(new FunctionScope(RuntimeInfo.LineNumber, new Stack<string>(RuntimeInfo.InParametersStack)));
|
||||
RuntimeInfo.InParametersStack.Clear();
|
||||
|
||||
if (RuntimeInfo.Functions.ContainsKey(key))
|
||||
int targetLine = FindBlockBoundary(RuntimeInfo.LineNumber);
|
||||
if (targetLine < 0)
|
||||
{
|
||||
RuntimeInfo.LineNumber = RuntimeInfo.Functions[key];
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.SearchFunction = key;
|
||||
}
|
||||
RuntimeInfo.Exit(ExitMessages.NoMatchingEndIf, true);
|
||||
return;
|
||||
}
|
||||
|
||||
[Statement("end", SearchMode.Exact, SpaceAround.None, ConsoleColor.Red, ExecuteInSearchMode = true)]
|
||||
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 _)
|
||||
{
|
||||
if (RuntimeInfo.IsSearching)
|
||||
{
|
||||
RuntimeInfo.IsInFunction = false;
|
||||
if (RuntimeInfo.IsLocalSearch)
|
||||
{
|
||||
RuntimeInfo.Exit($"Label \"{RuntimeInfo.SearchLabel}\" not found", true);
|
||||
HandleExit(ExitMessages.PlannedTermination, false);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.IsInFunction = false;
|
||||
}
|
||||
|
||||
RuntimeInfo.Exit("Planned termination by code", false);
|
||||
}
|
||||
|
||||
[Statement("trm", SearchMode.Exact, SpaceAround.None, ConsoleColor.Red, ExecuteInSearchMode = true)]
|
||||
[Statement("abort_all", 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);
|
||||
HandleExit(ExitMessages.PlannedTerminationCancelingTasks, true);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.IsInFunction = false;
|
||||
}
|
||||
|
||||
RuntimeInfo.Exit("Planned termination by code. Canceling all tasks", true);
|
||||
}
|
||||
|
||||
[Statement("trw", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Red)]
|
||||
[Statement("throw", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Red)]
|
||||
public void Throw(string message)
|
||||
{
|
||||
RuntimeInfo.Exit(message, true);
|
||||
}
|
||||
|
||||
[Statement("err", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Red)]
|
||||
[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<string>(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;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
[Statement("cwl", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkGreen, Priority = Priority.VeryLow)]
|
||||
[Statement("print_line", SearchMode.Exact, SpaceAround.None, ConsoleColor.DarkGreen, Priority = Priority.VeryLow)]
|
||||
public void WriteLineEmpty(string _)
|
||||
{
|
||||
RuntimeInfo.WriteLine(string.Empty);
|
||||
}
|
||||
|
||||
[Statement("print_line", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkGreen, Priority = Priority.VeryLow)]
|
||||
public void WriteLine(string args)
|
||||
{
|
||||
RuntimeInfo.WriteLine(args);
|
||||
}
|
||||
|
||||
[Statement("cw", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkGreen, Priority = Priority.VeryLow)]
|
||||
[Statement("print", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkGreen, Priority = Priority.VeryLow)]
|
||||
public void Write(string args)
|
||||
{
|
||||
RuntimeInfo.Write(args);
|
||||
}
|
||||
|
||||
[Statement("%crl", SearchMode.Contains, SpaceAround.End, ConsoleColor.DarkGreen, KeepStatementInArgs = true, Priority = Priority.Highest)]
|
||||
[Statement("%read_line", SearchMode.Contains, SpaceAround.None, ConsoleColor.DarkGreen, KeepStatementInArgs = true, Priority = Priority.Highest)]
|
||||
public void ReadLine(string args)
|
||||
{
|
||||
args += " ";
|
||||
while (args.Contains("%crl "))
|
||||
while (args.Contains("%read_line"))
|
||||
{
|
||||
string input = Console.ReadLine();
|
||||
if (input is null)
|
||||
{
|
||||
RuntimeInfo.Exit("Terminated by external process", true);
|
||||
RuntimeInfo.Exit(ExitMessages.TerminatedByExternalProcess, true);
|
||||
return;
|
||||
}
|
||||
args = args.ReplaceFirstOccurrence("%crl ", input.ToSaveString() + " ");
|
||||
args = args.ReplaceFirstOccurrence("%read_line ", input.ToSafeString() + " ");
|
||||
}
|
||||
RuntimeInfo.CurrentLine = args.TrimEnd();
|
||||
}
|
||||
|
||||
[Statement("%cr", SearchMode.Contains, SpaceAround.End, ConsoleColor.DarkGreen, KeepStatementInArgs = true, Priority = Priority.Highest)]
|
||||
[Statement("%read_key", SearchMode.Contains, SpaceAround.None, ConsoleColor.DarkGreen, KeepStatementInArgs = true, Priority = Priority.Highest)]
|
||||
public void ReadKey(string args)
|
||||
{
|
||||
args += " ";
|
||||
while (args.Contains("%cr "))
|
||||
while (args.Contains("%read_key"))
|
||||
{
|
||||
string input = ConsoleExtentions.ReadKey(RuntimeInfo).ToString();
|
||||
args = args.ReplaceFirstOccurrence("%cr ", input.ToSaveString() + " ");
|
||||
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)]
|
||||
[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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,39 @@
|
||||
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
|
||||
{
|
||||
[Statement("fnc", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, ExecuteInSearchMode = true)]
|
||||
[Statement("func", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, ExecuteInSearchMode = true, Separator = ":")]
|
||||
public void FindFunction(string args)
|
||||
{
|
||||
if (RuntimeInfo.InternalIsInFunction)
|
||||
{
|
||||
RuntimeInfo.Exit("Nested functions are not allowed", true);
|
||||
RuntimeInfo.Exit(ExitMessages.NestedFunctionsNotAllowed, true);
|
||||
return;
|
||||
}
|
||||
|
||||
string key = args.Trim();
|
||||
if (RuntimeInfo.Functions.ContainsKey(key))
|
||||
string functionDeclaration = args.Trim();
|
||||
if (!functionDeclaration.EndsWith(':'))
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidSyntaxColonRequired, true);
|
||||
return;
|
||||
}
|
||||
|
||||
string key = NormalizeBlockName(functionDeclaration);
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
|
||||
return;
|
||||
}
|
||||
|
||||
RuntimeInfo.Functions[key] = RuntimeInfo.LineNumber;
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.Functions.Add(key, RuntimeInfo.LineNumber);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(RuntimeInfo.SearchFunction) && RuntimeInfo.SearchFunction == key)
|
||||
{
|
||||
@@ -36,65 +43,51 @@ namespace YesNt.Interpreter.Statements
|
||||
RuntimeInfo.IsInFunction = true;
|
||||
}
|
||||
|
||||
[Statement("in", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Yellow)]
|
||||
[Statement("push_in", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Yellow)]
|
||||
public void AddInParameter(string args)
|
||||
{
|
||||
RuntimeInfo.InParametersStack.Push(args);
|
||||
}
|
||||
|
||||
[Statement("out", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Yellow)]
|
||||
[Statement("%out", SearchMode.Contains, SpaceAround.None, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)]
|
||||
public void GetOutParameter(string args)
|
||||
{
|
||||
if (RuntimeInfo.OutParametersStack.Count == 0)
|
||||
{
|
||||
RuntimeInfo.Exit("No out argument in stack", true);
|
||||
return;
|
||||
RuntimeInfo.CurrentLine = TemplateProcessor.ProcessStackParameters(args, "%out", RuntimeInfo.OutParametersStack, RuntimeInfo, ExitMessages.NoOutArgumentInStack);
|
||||
}
|
||||
|
||||
if (RuntimeInfo.Variables.ContainsKey(args))
|
||||
[Statement("%has_out", SearchMode.Contains, SpaceAround.None, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)]
|
||||
public void CheckIfOutParameterAvailable(string args)
|
||||
{
|
||||
RuntimeInfo.Variables[args] = RuntimeInfo.OutParametersStack.Pop();
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.Variables.Add(args, RuntimeInfo.OutParametersStack.Pop());
|
||||
}
|
||||
}
|
||||
|
||||
[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());
|
||||
args = args.Replace("%has_out", (RuntimeInfo.OutParametersStack.Count > 0).ToString());
|
||||
|
||||
RuntimeInfo.CurrentLine = args.TrimEnd();
|
||||
}
|
||||
|
||||
[Statement("cal", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, Priority = Priority.Low, Seperator = "|")]
|
||||
[Statement("call", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, Priority = Priority.Low, Separator = " with ")]
|
||||
public void Call(string args)
|
||||
{
|
||||
string[] parts = args.Split('|');
|
||||
string[] parts = args.Split(" with ", 2, StringSplitOptions.None);
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
RuntimeInfo.Exit("Invalid syntax", true);
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
|
||||
return;
|
||||
}
|
||||
|
||||
string key = parts[0].Trim();
|
||||
string[] functionArgumets = parts[1].Split(',');
|
||||
string key = NormalizeBlockName(parts[0]);
|
||||
string[] functionArguments = parts[1].Split(',');
|
||||
|
||||
foreach (string argumanet in functionArgumets)
|
||||
foreach (string argument in functionArguments)
|
||||
{
|
||||
RuntimeInfo.InParametersStack.Push(argumanet.Trim());
|
||||
RuntimeInfo.InParametersStack.Push(argument.Trim());
|
||||
}
|
||||
|
||||
RuntimeInfo.FunctionCallStack.Push(new FunctionScope(RuntimeInfo.LineNumber, new Stack<string>(RuntimeInfo.InParametersStack)));
|
||||
RuntimeInfo.InParametersStack.Clear();
|
||||
RuntimeInfo.CurrentLine = string.Empty;
|
||||
|
||||
if (RuntimeInfo.Functions.ContainsKey(key))
|
||||
if (RuntimeInfo.Functions.TryGetValue(key, out int value))
|
||||
{
|
||||
RuntimeInfo.LineNumber = RuntimeInfo.Functions[key];
|
||||
RuntimeInfo.LineNumber = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -102,64 +95,50 @@ namespace YesNt.Interpreter.Statements
|
||||
}
|
||||
}
|
||||
|
||||
[Statement("get", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Yellow)]
|
||||
[Statement("%in", SearchMode.Contains, SpaceAround.None, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)]
|
||||
public void GetInParameter(string args)
|
||||
{
|
||||
if (!RuntimeInfo.IsInFunction)
|
||||
{
|
||||
RuntimeInfo.Exit("Statement not allowed outside of function", true);
|
||||
RuntimeInfo.Exit(ExitMessages.StatementNotAllowedOutsideFunction, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (RuntimeInfo.FunctionCallStack.Peek().Arguemtns.Count == 0)
|
||||
{
|
||||
RuntimeInfo.Exit("No in argument in stack", true);
|
||||
return;
|
||||
RuntimeInfo.CurrentLine = TemplateProcessor.ProcessStackParameters(args, "%in", RuntimeInfo.FunctionCallStack.Peek().Arguments, RuntimeInfo, ExitMessages.NoInArgumentInStack);
|
||||
}
|
||||
|
||||
if (RuntimeInfo.Variables.ContainsKey(args))
|
||||
{
|
||||
RuntimeInfo.Variables[args] = RuntimeInfo.FunctionCallStack.Peek().Arguemtns.Pop();
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.Variables.Add(args, RuntimeInfo.FunctionCallStack.Peek().Arguemtns.Pop());
|
||||
}
|
||||
}
|
||||
|
||||
[Statement("%isi", SearchMode.Contains, SpaceAround.End, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)]
|
||||
public void CheckIfInParameterAvalible(string args)
|
||||
[Statement("%has_in", SearchMode.Contains, SpaceAround.None, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)]
|
||||
public void CheckIfInParameterAvailable(string args)
|
||||
{
|
||||
if (!RuntimeInfo.IsInFunction)
|
||||
{
|
||||
RuntimeInfo.Exit("Statement not allowed outside of function", true);
|
||||
RuntimeInfo.Exit(ExitMessages.StatementNotAllowedOutsideFunction, true);
|
||||
return;
|
||||
}
|
||||
|
||||
args += " ";
|
||||
args = args.Replace("%isi", (RuntimeInfo.FunctionCallStack.Peek().Arguemtns.Count > 0).ToString());
|
||||
args = args.Replace("%has_in", (RuntimeInfo.FunctionCallStack.Peek().Arguments.Count > 0).ToString());
|
||||
|
||||
RuntimeInfo.CurrentLine = args.TrimEnd();
|
||||
}
|
||||
|
||||
[Statement("put", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Yellow)]
|
||||
[Statement("push_out", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Yellow)]
|
||||
public void AddOutParameter(string args)
|
||||
{
|
||||
if (!RuntimeInfo.IsInFunction)
|
||||
{
|
||||
RuntimeInfo.Exit("Statement not allowed outside of function", true);
|
||||
RuntimeInfo.Exit(ExitMessages.StatementNotAllowedOutsideFunction, true);
|
||||
return;
|
||||
}
|
||||
|
||||
RuntimeInfo.FunctionCallStack.Peek().Results.Push(args);
|
||||
}
|
||||
|
||||
[Statement("ret", SearchMode.Exact, SpaceAround.None, ConsoleColor.DarkYellow, ExecuteInSearchMode = true)]
|
||||
[Statement("return", SearchMode.Exact, SpaceAround.None, ConsoleColor.DarkYellow, ExecuteInSearchMode = true)]
|
||||
public void Return(string _)
|
||||
{
|
||||
if (!RuntimeInfo.IsInFunction)
|
||||
{
|
||||
RuntimeInfo.Exit("Statement not allowed outside of function", true);
|
||||
RuntimeInfo.Exit(ExitMessages.StatementNotAllowedOutsideFunction, true);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -169,14 +148,12 @@ namespace YesNt.Interpreter.Statements
|
||||
|
||||
if (RuntimeInfo.IsLocalSearch)
|
||||
{
|
||||
RuntimeInfo.Exit($"Label \"{RuntimeInfo.SearchLabel}\" not found", true);
|
||||
RuntimeInfo.Exit(ExitMessages.LabelNotFound(RuntimeInfo.SearchLabel), true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
RuntimeInfo.IsInFunction = false;
|
||||
}
|
||||
|
||||
if (RuntimeInfo.FunctionCallStack.Count > 0)
|
||||
{
|
||||
@@ -187,14 +164,13 @@ namespace YesNt.Interpreter.Statements
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.Exit("No function in stack", true);
|
||||
RuntimeInfo.Exit(ExitMessages.NoFunctionInStack, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Statement("ccs", SearchMode.Exact, SpaceAround.None, ConsoleColor.Red)]
|
||||
[Statement("clear_call_stack", SearchMode.Exact, SpaceAround.None, ConsoleColor.Red)]
|
||||
public void ClearCallStack(string _)
|
||||
{
|
||||
RuntimeInfo.FunctionCallStack.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,83 +9,68 @@ using YesNt.Interpreter.Enums;
|
||||
using YesNt.Interpreter.Runtime;
|
||||
using YesNt.Interpreter.Utilities;
|
||||
|
||||
namespace YesNt.Interpreter.Statements
|
||||
{
|
||||
internal class ProcessingStatements : StatementRuntimeInformation
|
||||
{
|
||||
private static readonly Regex calculationRegex = new Regex(@"[0-9*+().,^%/-]+[0-9*+ ().,^%/-]+[0-9*+().,^%/-]+");
|
||||
namespace YesNt.Interpreter.Statements;
|
||||
|
||||
[Statement("!calc", SearchMode.EndOfLine, SpaceAround.Start, ConsoleColor.DarkYellow, Priority = Priority.High)]
|
||||
internal partial class ProcessingStatements : StatementRuntimeInformation
|
||||
{
|
||||
[Statement("calc", SearchMode.EndOfLine, SpaceAround.Start, ConsoleColor.DarkYellow, Priority = Priority.High)]
|
||||
public void Calculate(string args)
|
||||
{
|
||||
MatchCollection matches = calculationRegex.Matches(args.FromSaveString());
|
||||
|
||||
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 = TemplateProcessor.ProcessCalculations(args.FromSafeString(), RuntimeInfo, CalculationRegex());
|
||||
}
|
||||
|
||||
RuntimeInfo.CurrentLine = args;
|
||||
}
|
||||
|
||||
[Statement("!eval", SearchMode.EndOfLine, SpaceAround.Start, ConsoleColor.DarkYellow, Priority = Priority.VeryHigh)]
|
||||
[Statement("eval", SearchMode.EndOfLine, SpaceAround.Start, ConsoleColor.DarkYellow, Priority = Priority.VeryHigh)]
|
||||
public void Evaluate(string args)
|
||||
{
|
||||
RuntimeInfo.CurrentLine = args.FromSaveString();
|
||||
RuntimeInfo.CurrentLine = args.FromSafeString();
|
||||
}
|
||||
|
||||
[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)]
|
||||
[Statement("task", SearchMode.EndOfLine, SpaceAround.Start, ConsoleColor.DarkYellow, Priority = Priority.VeryHigh)]
|
||||
public void RunTask(string line)
|
||||
{
|
||||
int lineNumer = RuntimeInfo.LineNumber;
|
||||
int lineNumber = RuntimeInfo.LineNumber;
|
||||
List<Line> lines = RuntimeInfo.Lines.GetRange(0, RuntimeInfo.Lines.Count);
|
||||
|
||||
Line oldLine = lines[lineNumer];
|
||||
Line oldLine = lines[lineNumber];
|
||||
|
||||
lines[lineNumer] = new Line(line, oldLine.FileName, oldLine.LineNumber);
|
||||
lines[lineNumber] = new Line(line, oldLine.FileName, oldLine.LineNumber);
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
YesNtInterpreter interpreter = new YesNtInterpreter();
|
||||
interpreter.Initialize();
|
||||
interpreter.Execute(lines, RuntimeInfo.GloablVariables, lineNumer, RuntimeInfo);
|
||||
interpreter.Execute(lines, RuntimeInfo.GlobalVariables, lineNumber, RuntimeInfo);
|
||||
});
|
||||
|
||||
RuntimeInfo.CurrentLine = string.Empty;
|
||||
}
|
||||
|
||||
[Statement("slp", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Magenta)]
|
||||
[Statement("sleep", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Magenta)]
|
||||
public void Sleep(string args)
|
||||
{
|
||||
_ = int.TryParse(args, out int millisecondsTimeout);
|
||||
ConsoleExtentions.Sleep(millisecondsTimeout, RuntimeInfo);
|
||||
if (int.TryParse(args, out int millisecondsTimeout))
|
||||
{
|
||||
ConsoleExtensions.Sleep(millisecondsTimeout, RuntimeInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidTimeoutValue(args), true);
|
||||
}
|
||||
}
|
||||
|
||||
[Statement("imp", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Magenta)]
|
||||
[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");
|
||||
@@ -102,17 +87,20 @@ namespace YesNt.Interpreter.Statements
|
||||
{
|
||||
RuntimeInfo.Lines.Insert(RuntimeInfo.LineNumber + i, new Line(lines[i], Path.GetFileName(path), i));
|
||||
}
|
||||
RuntimeInfo.PreScanLinesAction?.Invoke();
|
||||
RuntimeInfo.LineNumber--;
|
||||
}
|
||||
catch
|
||||
{
|
||||
RuntimeInfo.Exit($"Could not load file \"{path}\"", true);
|
||||
RuntimeInfo.Exit(ExitMessages.CouldNotLoadFile(path), true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.Exit($"Could not find file \"{path}\"", true);
|
||||
}
|
||||
RuntimeInfo.Exit(ExitMessages.CouldNotFindFile(path), true);
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex("[0-9*+().,^%/-]+[0-9*+ ().,^%/-]+[0-9*+().,^%/-]+")]
|
||||
private static partial Regex CalculationRegex();
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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<string> 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<string> 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<string> outputStack = new Stack<string>();
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,27 @@
|
||||
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("<", SearchMode.StartOfLine, SpaceAround.None, Priority = Priority.VeryLow)]
|
||||
[Statement("var", SearchMode.StartOfLine, SpaceAround.End, System.ConsoleColor.DarkBlue, Priority = Priority.VeryLow, Separator = "=")]
|
||||
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
|
||||
{
|
||||
RuntimeInfo.Exit("Invalid syntax", true);
|
||||
}
|
||||
}
|
||||
|
||||
[Statement("!<", SearchMode.StartOfLine, SpaceAround.None, Priority = Priority.VeryLow)]
|
||||
[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<string, string> dict, string args)
|
||||
{
|
||||
string[] parts = args.Split('=');
|
||||
if (parts.Length == 2)
|
||||
@@ -45,77 +29,39 @@ namespace YesNt.Interpreter.Statements
|
||||
string key = parts[0].Trim();
|
||||
if (key.Contains(' '))
|
||||
{
|
||||
RuntimeInfo.Exit("Invalid Syntax", true);
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
|
||||
}
|
||||
|
||||
if (RuntimeInfo.GloablVariables.ContainsKey(key))
|
||||
{
|
||||
RuntimeInfo.GloablVariables[key] = parts[1].Trim();
|
||||
dict[key] = parts[1].Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.GloablVariables.Add(key, parts[1].Trim());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.Exit("Invalid syntax", true);
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Statement("del", SearchMode.StartOfLine, SpaceAround.End, System.ConsoleColor.Red, Priority = Priority.VeryLow)]
|
||||
[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);
|
||||
_ = RuntimeInfo.Variables.Remove(key);
|
||||
}
|
||||
else if (RuntimeInfo.GloablVariables.ContainsKey(key))
|
||||
else if (RuntimeInfo.GlobalVariables.ContainsKey(key))
|
||||
{
|
||||
RuntimeInfo.GloablVariables.Remove(key);
|
||||
_ = RuntimeInfo.GlobalVariables.Remove(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.Exit($"Variable \"{key}\" not found", true);
|
||||
RuntimeInfo.Exit(ExitMessages.VariableNotFound(key), true);
|
||||
}
|
||||
}
|
||||
|
||||
[Statement(">", SearchMode.Contains, SpaceAround.None, Priority = Priority.Highest)]
|
||||
[Statement("${", SearchMode.Contains, SpaceAround.None, Priority = Priority.Highest, Separator = "}")]
|
||||
public void ReadVariable(string _)
|
||||
{
|
||||
if (!RuntimeInfo.CurrentLine.Contains('>'))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, string> variable in RuntimeInfo.Variables)
|
||||
{
|
||||
RuntimeInfo.CurrentLine = RuntimeInfo.CurrentLine.Replace($">{variable.Key}", variable.Value);
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<string, string> 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.CurrentLine = TemplateProcessor.ProcessVariables(RuntimeInfo.CurrentLine, RuntimeInfo);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,31 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace YesNt.Interpreter.Utilities
|
||||
{
|
||||
internal static class Evaluator
|
||||
namespace YesNt.Interpreter.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Provides expression evaluation used by conditional and arithmetic statements.
|
||||
/// </summary>
|
||||
internal static partial class Evaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluates a boolean condition string such as <c>a == b</c>, <c>x > 3</c>, or <c>true</c>.
|
||||
/// </summary>
|
||||
/// <param name="input">The condition expression, which may contain safe-string encoded values.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true"/> or <see langword="false"/> if the condition could be evaluated;
|
||||
/// <see langword="null"/> if the expression is not a recognized condition form (treated as an error by callers).
|
||||
/// </returns>
|
||||
public static bool? EvaluateCondition(string input)
|
||||
{
|
||||
if (input.ToLower().FromSaveString().Trim() == "true")
|
||||
input = input.FromSafeString();
|
||||
string lower = input.ToLower().Trim();
|
||||
if (lower == "true")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (input.ToLower().FromSaveString().Trim() == "false")
|
||||
else if (lower == "false")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -20,16 +33,16 @@ namespace YesNt.Interpreter.Utilities
|
||||
string[] parts = input.Split("==");
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
string part1 = parts[0].FromSaveString().Trim();
|
||||
string part2 = parts[1].FromSaveString().Trim();
|
||||
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();
|
||||
string part1 = parts[0].Trim();
|
||||
string part2 = parts[1].Trim();
|
||||
return part1 != part2;
|
||||
}
|
||||
|
||||
@@ -38,12 +51,7 @@ namespace YesNt.Interpreter.Utilities
|
||||
{
|
||||
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 succ1 && succ2 && part1 >= part2;
|
||||
}
|
||||
|
||||
parts = input.Split("<=");
|
||||
@@ -51,12 +59,7 @@ namespace YesNt.Interpreter.Utilities
|
||||
{
|
||||
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 succ1 && succ2 && part1 <= part2;
|
||||
}
|
||||
|
||||
parts = input.Split(">");
|
||||
@@ -64,12 +67,7 @@ namespace YesNt.Interpreter.Utilities
|
||||
{
|
||||
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 succ1 && succ2 && part1 > part2;
|
||||
}
|
||||
|
||||
parts = input.Split("<");
|
||||
@@ -77,52 +75,59 @@ namespace YesNt.Interpreter.Utilities
|
||||
{
|
||||
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 succ1 && succ2 && part1 < part2;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates a numeric arithmetic expression string and returns the result as a string.
|
||||
/// Supports <c>+</c>, <c>-</c>, <c>*</c>, <c>/</c>, <c>%</c> (modulo), and <c>^</c> (power) operators
|
||||
/// with standard precedence (<c>^</c> highest, <c>+</c>/<c>-</c> lowest) and parentheses.
|
||||
/// Adjacent sign characters (<c>++</c>, <c>--</c>, <c>-+</c>, <c>+-</c>) are normalized before evaluation.
|
||||
/// </summary>
|
||||
/// <param name="input">The arithmetic expression to evaluate.</param>
|
||||
/// <returns>The result as a culture-invariant numeric string, or <c>"NaN"</c> if evaluation failed.</returns>
|
||||
public static string Calculate(string input)
|
||||
{
|
||||
input = Regex.Replace(input, @"(\+ +\+)+", "+");
|
||||
input = Regex.Replace(input, @"(\- +\-)+", "+");
|
||||
input = Regex.Replace(input, @"(\- +\+)+", "-");
|
||||
input = Regex.Replace(input, @"(\+ +\-)+", "-");
|
||||
input = input.FromSafeString();
|
||||
input = PlusPlusRegex().Replace(input, "+");
|
||||
input = MinusMinusRegex().Replace(input, "+");
|
||||
input = MinusPlusRegex().Replace(input, "-");
|
||||
input = PlusMinusRegex().Replace(input, "-");
|
||||
|
||||
string yes = Calculate(input, '+');
|
||||
return yes;
|
||||
return CalculateInternal(input, '+');
|
||||
}
|
||||
|
||||
private static string Calculate(string input, char op)
|
||||
private static string CalculateInternal(string input, char op)
|
||||
{
|
||||
if (input is null)
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
input = input.FromSaveString();
|
||||
if (input.ToStandardizedNumber(out double quickNum))
|
||||
{
|
||||
return quickNum.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
MatchCollection matches = Regex.Matches(input, @"\(([^()]+)\)");
|
||||
MatchCollection matches = ParenthesesRegex().Matches(input);
|
||||
while (matches.Count > 0)
|
||||
{
|
||||
for (int i = 0; i < matches.Count; i++)
|
||||
{
|
||||
string calc = matches[i].Value.Substring(1, matches[i].Length - 2);
|
||||
string ret = Calculate(calc);
|
||||
string ret = CalculateInternal(calc, '+');
|
||||
input = input.Replace(matches[i].Value, ret);
|
||||
}
|
||||
matches = Regex.Matches(input, @"\(([^()]+)\)");
|
||||
matches = ParenthesesRegex().Matches(input);
|
||||
}
|
||||
|
||||
string[] parts = input.Split(op);
|
||||
|
||||
//Weird fix
|
||||
// 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]}";
|
||||
@@ -137,11 +142,11 @@ namespace YesNt.Interpreter.Utilities
|
||||
|
||||
part = op switch
|
||||
{
|
||||
'+' => Calculate(part, '-'),
|
||||
'-' => Calculate(part, '*'),
|
||||
'*' => Calculate(part, '/'),
|
||||
'/' => Calculate(part, '%'),
|
||||
'%' => Calculate(part, '^'),
|
||||
'+' => CalculateInternal(part, '-'),
|
||||
'-' => CalculateInternal(part, '*'),
|
||||
'*' => CalculateInternal(part, '/'),
|
||||
'/' => CalculateInternal(part, '%'),
|
||||
'%' => CalculateInternal(part, '^'),
|
||||
_ => part
|
||||
};
|
||||
|
||||
@@ -194,5 +199,19 @@ namespace YesNt.Interpreter.Utilities
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>Represents the method that handles the <see cref="FixedProcess.OutputDataReceived"/> and <see cref="FixedProcess.ErrorDataReceived"/> events.</summary>
|
||||
public delegate void DataReceivedEventHandler(object sender, DataReceivedEventArgs e);
|
||||
|
||||
internal delegate void UserCallBack(string data);
|
||||
|
||||
/// <summary>
|
||||
/// A workaround replacement for <see cref="System.Diagnostics.Process"/> that fixes a buffering
|
||||
/// issue in <see cref="System.Diagnostics.Process.BeginOutputReadLine"/> / <see cref="System.Diagnostics.Process.BeginErrorReadLine"/>:
|
||||
/// 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.
|
||||
/// <see cref="FixedProcess"/> flushes whatever is in the read buffer immediately, enabling real-time
|
||||
/// output forwarding for interactive child processes.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Provides data for the <see cref="FixedProcess.OutputDataReceived"/> and <see cref="FixedProcess.ErrorDataReceived"/> events.</summary>
|
||||
public class DataReceivedEventArgs : EventArgs
|
||||
{
|
||||
internal string _data;
|
||||
|
||||
/// <summary>Gets the line of characters that was written to a redirected <see cref="T:System.Diagnostics.Process" /> output stream.</summary>
|
||||
/// <returns>The line that was written by an associated <see cref="T:System.Diagnostics.Process" /> to its redirected <see cref="P:System.Diagnostics.Process.StandardOutput" /> or <see cref="P:System.Diagnostics.Process.StandardError" /> stream.</returns>
|
||||
/// <filterpriority>2</filterpriority>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace YesNt.Interpreter.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for string manipulation used throughout the interpreter.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// 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 → <c>\x01spc\x01</c>, newline → <c>\x01nli\x01</c>). These codes use the
|
||||
/// non-printable SOH character (U+0001) as a sentinel. Written via string concatenation
|
||||
/// (<c>"\x01" + "spc" + "\x01"</c>) to avoid C#'s greedy <c>\x</c> 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 <see cref="ReplacementRules"/>. Use <see cref="ToSafeString"/>
|
||||
/// to encode and <see cref="FromSafeString"/> to decode.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class StringExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static Dictionary<string, string> 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<string, string> reverseReplacementRules = ReplacementRules.ToDictionary(x => x.Value, x => x.Key);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="input">The plain string to encode.</param>
|
||||
/// <returns>The safe-string encoded representation.</returns>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a safe-string back to its original plain-text form.
|
||||
/// </summary>
|
||||
/// <param name="input">A safe-string encoded string.</param>
|
||||
/// <returns>The decoded plain string.</returns>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse the string as a <see cref="double"/>, first decoding safe-string encoding
|
||||
/// and normalising decimal separators (comma → period).
|
||||
/// </summary>
|
||||
/// <param name="input">The string to parse (may be safe-string encoded).</param>
|
||||
/// <param name="result">When this method returns, contains the parsed value if successful.</param>
|
||||
/// <returns><see langword="true"/> if parsing succeeded; otherwise <see langword="false"/>.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Replaces only the first occurrence of <paramref name="oldValue"/> in the string.</summary>
|
||||
/// <param name="input">The source string.</param>
|
||||
/// <param name="oldValue">The substring to find.</param>
|
||||
/// <param name="newValue">The replacement value.</param>
|
||||
/// <returns>A new string with the first occurrence replaced.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Replaces only the last occurrence of <paramref name="oldValue"/> in the string.</summary>
|
||||
/// <param name="input">The source string.</param>
|
||||
/// <param name="oldValue">The substring to find.</param>
|
||||
/// <param name="newValue">The replacement value.</param>
|
||||
/// <returns>A new string with the last occurrence replaced.</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Counts the number of trailing whitespace characters in the string.</summary>
|
||||
/// <param name="input">The source string.</param>
|
||||
/// <returns>The number of whitespace characters at the end of the string.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Provides high-performance template substitution for variables and stack parameters.
|
||||
/// </summary>
|
||||
internal static class TemplateProcessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Replaces all occurrences of ${variableName} with their current values.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces all occurrences of a placeholder (e.g., %in, %out) with values popped from a stack.
|
||||
/// </summary>
|
||||
public static string ProcessStackParameters(string input, string placeholder, Stack<string> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces all occurrences of a placeholder with a fixed value.
|
||||
/// </summary>
|
||||
public static string ProcessSimplePlaceholders(string input, string placeholder, string value)
|
||||
{
|
||||
return string.IsNullOrEmpty(input) ? input : input.Replace(placeholder, value, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces all occurrences of a placeholder with values generated by a provider function.
|
||||
/// </summary>
|
||||
public static string ProcessDynamicPlaceholders(string input, string placeholder, Func<string> 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces all occurrences of arithmetic expressions with their results.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,61 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net5.0</TargetFramework>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<RootNamespace>YesNt.Interpreter</RootNamespace>
|
||||
<ApplicationIcon />
|
||||
<OutputType>Exe</OutputType>
|
||||
<OutputType>Library</OutputType>
|
||||
<StartupObject />
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<RepositoryUrl>https://github.com/Stone-Red-Code/YesNt-Interpreter/</RepositoryUrl>
|
||||
<PackageTags>scripting, modding, language</PackageTags>
|
||||
<PackageIcon>Logo.png</PackageIcon>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<IsAotCompatible>True</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<IsAotCompatible>True</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<IsAotCompatible>True</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<IsAotCompatible>True</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="..\assets\Logo.png">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
<None Include="..\LICENSE">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
<None Include="..\README.md">
|
||||
<Pack>True</Pack>
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="SonarAnalyzer.CSharp" Version="10.20.0.135146">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YesNt.Interpreter.Generator\YesNt.Interpreter.Generator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,26 @@
|
||||
<svg width="255" height="255" viewBox="0 0 255 255" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_i_5_2)">
|
||||
<g clip-path="url(#clip0_5_2)">
|
||||
<rect width="255" height="255" rx="50" fill="#AF3E3E"/>
|
||||
<path d="M43.0906 27.7405L51.2733 32.9156L50.2185 70.8102L50.91 71.2476L84.6956 54.0531L92.8783 59.2281L49.6179 80.4339L34.2385 104.751L27.0931 100.232L42.4724 75.9148L43.0906 27.7405ZM75.5918 132.196C71.3275 129.499 68.2444 126.231 66.3424 122.393C64.4717 118.548 63.7759 114.397 64.255 109.94C64.7655 105.476 66.4541 100.978 69.3211 96.4445C72.188 91.9114 75.5407 88.4384 79.3793 86.0257C83.2492 83.6059 87.2862 82.3674 91.4905 82.3103C95.726 82.2462 99.8415 83.4775 103.837 86.0043C106.142 87.4621 108.175 89.2858 109.936 91.4754C111.698 93.665 112.954 96.2073 113.705 99.1023C114.467 101.978 114.503 105.174 113.812 108.69C113.122 112.206 111.47 116.029 108.858 120.159L107.036 123.04L72.9224 101.465L76.6397 95.5873L103.839 112.789C105.418 110.292 106.328 107.748 106.568 105.157C106.827 102.578 106.403 100.145 105.295 97.8577C104.206 95.5825 102.432 93.6674 99.9737 92.1125C97.2654 90.3996 94.4968 89.5899 91.668 89.6832C88.8705 89.7695 86.2694 90.5446 83.8645 92.0086C81.4596 93.4725 79.4858 95.4242 77.943 97.8637L75.4648 101.782C73.351 105.124 72.1354 108.322 71.818 111.375C71.532 114.421 72.0435 117.205 73.3527 119.727C74.674 122.23 76.708 124.35 79.4548 126.087C81.2412 127.217 83.0126 127.988 84.7691 128.4C86.5569 128.804 88.3009 128.832 90.0012 128.482C91.7137 128.113 93.3439 127.342 94.892 126.17L100.295 132.168C98.1944 133.959 95.7932 135.183 93.0914 135.841C90.4019 136.48 87.5589 136.51 84.5627 135.933C81.5785 135.336 78.5882 134.091 75.5918 132.196ZM157.912 134.886L150.71 132.751C150.97 131.49 151.029 130.143 150.887 128.709C150.777 127.267 150.288 125.829 149.421 124.393C148.554 122.958 147.132 121.614 145.153 120.363C142.445 118.65 139.793 117.847 137.198 117.954C134.634 118.053 132.751 119.054 131.548 120.955C130.479 122.646 130.249 124.369 130.859 126.126C131.469 127.884 132.874 129.915 135.073 132.22L140.622 137.988C143.971 141.451 145.99 144.785 146.679 147.99C147.38 151.177 146.752 154.316 144.797 157.409C143.193 159.944 141.03 161.749 138.307 162.823C135.603 163.91 132.59 164.209 129.268 163.722C125.946 163.234 122.565 161.903 119.127 159.729C114.613 156.874 111.497 153.532 109.778 149.702C108.059 145.872 107.996 141.825 109.59 137.562L117.064 140.031C116.172 142.801 116.205 145.323 117.161 147.595C118.137 149.879 120.017 151.902 122.802 153.663C125.971 155.668 128.913 156.587 131.626 156.42C134.371 156.247 136.345 155.21 137.548 153.308C138.52 151.771 138.796 150.144 138.376 148.427C137.969 146.69 136.815 144.858 134.915 142.93L128.632 136.536C125.186 133.013 123.151 129.641 122.524 126.422C121.93 123.195 122.598 120.055 124.53 117.001C126.109 114.504 128.207 112.738 130.824 111.704C133.46 110.682 136.344 110.395 139.476 110.843C142.627 111.303 145.759 112.517 148.87 114.485C153.25 117.255 156.081 120.39 157.363 123.89C158.665 127.402 158.847 131.067 157.912 134.886Z" fill="black"/>
|
||||
<path d="M165.459 134.221L193.701 198.073L186.219 201.382L129.251 166.639L128.627 166.914L150.801 217.048L143.069 220.468L114.827 156.616L122.31 153.307L179.458 188.12L180.082 187.844L157.852 137.586L165.459 134.221ZM207.82 134.57L210.578 140.806L185.761 151.783L183.003 145.547L207.82 134.57ZM185.162 130.875L192.52 127.62L212.708 173.264C213.627 175.342 214.618 176.768 215.681 177.541C216.755 178.283 217.848 178.62 218.96 178.55C220.084 178.451 221.187 178.162 222.268 177.684C223.078 177.325 223.725 176.99 224.208 176.677C224.681 176.343 225.06 176.076 225.344 175.876L229.764 181.823C229.348 182.231 228.735 182.726 227.924 183.308C227.122 183.912 226.035 184.517 224.663 185.123C222.585 186.043 220.35 186.497 217.96 186.486C215.59 186.465 213.346 185.83 211.227 184.58C209.129 183.321 207.465 181.299 206.233 178.514L185.162 130.875Z" fill="black"/>
|
||||
<path d="M149.184 114.691C153.345 117.604 157.076 118.159 162.062 127.151L165.478 134.268L158.942 140.061C158.499 138.814 157.998 138.531 156.782 138.555C148.463 138.492 149.176 133.091 152.758 128.979L154.925 127.609L149.184 114.691Z" fill="black"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_i_5_2" x="0" y="0" width="255" height="255" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feMorphology radius="19" operator="erode" in="SourceAlpha" result="effect1_innerShadow_5_2"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feComposite in2="hardAlpha" operator="arithmetic" k2="-1" k3="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
|
||||
<feBlend mode="normal" in2="shape" result="effect1_innerShadow_5_2"/>
|
||||
</filter>
|
||||
<clipPath id="clip0_5_2">
|
||||
<rect width="255" height="255" rx="50" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,7 @@
|
||||
let i = 0;
|
||||
let s = 0;
|
||||
while (i < 200000) {
|
||||
i += 1;
|
||||
s += i;
|
||||
}
|
||||
console.log(s);
|
||||
@@ -0,0 +1,6 @@
|
||||
i = 0
|
||||
s = 0
|
||||
while i < 200000:
|
||||
i += 1
|
||||
s += i
|
||||
print(s)
|
||||
@@ -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}
|
||||
@@ -0,0 +1,8 @@
|
||||
function add_one(x) {
|
||||
return x + 1;
|
||||
}
|
||||
let i = 0;
|
||||
while (i < 60000) {
|
||||
i = add_one(i);
|
||||
}
|
||||
console.log(i);
|
||||
@@ -0,0 +1,7 @@
|
||||
def add_one(x):
|
||||
return x + 1
|
||||
|
||||
i = 0
|
||||
while i < 60000:
|
||||
i = add_one(i)
|
||||
print(i)
|
||||
@@ -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}
|
||||
@@ -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);
|
||||
@@ -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)
|
||||
@@ -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}
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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 <n>` | Jump to line `n` and switch to edit mode |
|
||||
| `save [path]` | Save to current path or a new path |
|
||||
| `load <path>` | 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.
|
||||
@@ -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 <name> = <value>
|
||||
```
|
||||
|
||||
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 <name> = <value>
|
||||
```
|
||||
|
||||
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 <name>
|
||||
```
|
||||
|
||||
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 <text>
|
||||
print_line
|
||||
```
|
||||
|
||||
Writes `<text>` 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 <text>
|
||||
```
|
||||
|
||||
Writes `<text>` 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.
|
||||
|
||||
```
|
||||
<expression> 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:
|
||||
|
||||
```
|
||||
<left> <operator> <right>
|
||||
```
|
||||
|
||||
| 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 <condition>:
|
||||
<body>
|
||||
else:
|
||||
<alternative>
|
||||
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 <condition>:
|
||||
<body>
|
||||
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 <name>:
|
||||
goto <name>
|
||||
```
|
||||
|
||||
`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 <condition> goto <label>
|
||||
```
|
||||
|
||||
Jumps to `<label>` only when the condition is true.
|
||||
|
||||
```ynt
|
||||
var n = 0
|
||||
label start:
|
||||
var n = ${n} + 1 calc
|
||||
if ${n} < 10 goto start
|
||||
print_line done
|
||||
```
|
||||
|
||||
### Conditional function call
|
||||
|
||||
```
|
||||
if <condition> call <function>
|
||||
```
|
||||
|
||||
Calls `<function>` only when the condition is true.
|
||||
|
||||
```ynt
|
||||
var debug = True
|
||||
if ${debug} == True call dump_state
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Functions
|
||||
|
||||
### Declaring a function - `func`
|
||||
|
||||
```
|
||||
func <name>:
|
||||
<body>
|
||||
return
|
||||
```
|
||||
|
||||
A function declaration registers the function name and the line it starts on.
|
||||
The body runs until `return` is reached.
|
||||
**Nested function declarations are not allowed.**
|
||||
|
||||
```ynt
|
||||
func add:
|
||||
var result = %in + %in calc
|
||||
push_out ${result}
|
||||
return
|
||||
```
|
||||
|
||||
### Calling a function - `call`
|
||||
|
||||
```
|
||||
call <name>
|
||||
call <name> with <arg1>, <arg2>, …
|
||||
```
|
||||
|
||||
`call` without `with` uses any values previously pushed with `push_in`.
|
||||
`call … with …` pushes the comma-separated arguments and then calls the function.
|
||||
|
||||
```ynt
|
||||
call greet
|
||||
call add with 3, 7
|
||||
```
|
||||
|
||||
### Input arguments
|
||||
|
||||
#### Push an input argument - `push_in`
|
||||
|
||||
```
|
||||
push_in <value>
|
||||
```
|
||||
|
||||
Pushes a value onto the input argument stack. Values are consumed in the order they were pushed -
|
||||
the first `push_in` call is the first value consumed by `%in` inside the function.
|
||||
|
||||
```ynt
|
||||
push_in Alice
|
||||
call greet
|
||||
```
|
||||
|
||||
#### Pop the next input argument - `%in`
|
||||
|
||||
`%in` is an inline token (only valid inside a function) that is replaced with the next value
|
||||
popped from the argument stack.
|
||||
|
||||
```ynt
|
||||
func greet:
|
||||
var name = %in
|
||||
print_line Hello, ${name}!
|
||||
return
|
||||
```
|
||||
|
||||
#### Check if input argument exists - `%has_in`
|
||||
|
||||
`%has_in` is replaced with `True` or `False` depending on whether the input stack is non-empty.
|
||||
Only valid inside a function.
|
||||
|
||||
```ynt
|
||||
func greet:
|
||||
if %has_in call do_greet
|
||||
return
|
||||
```
|
||||
|
||||
### Output values
|
||||
|
||||
#### Push an output value - `push_out`
|
||||
|
||||
```
|
||||
push_out <value>
|
||||
```
|
||||
|
||||
Only valid inside a function. Pushes a return value onto the output stack.
|
||||
|
||||
#### Consume an output value - `%out`
|
||||
|
||||
`%out` is an inline token that pops and inserts the top value from the output stack.
|
||||
Valid anywhere after a function call or list/processing statement that pushes to the output stack.
|
||||
|
||||
```ynt
|
||||
call add with 3, 7
|
||||
var total = %out
|
||||
print_line ${total}
|
||||
```
|
||||
|
||||
#### Check if output value exists - `%has_out`
|
||||
|
||||
`%has_out` is replaced with `True` or `False` depending on whether the output stack is non-empty.
|
||||
|
||||
```ynt
|
||||
call maybe_produce
|
||||
if %has_out call consume_result
|
||||
```
|
||||
|
||||
### Clear the call stack - `clear_call_stack`
|
||||
|
||||
```
|
||||
clear_call_stack
|
||||
```
|
||||
|
||||
Discards all frames on the function call stack. Useful for error recovery.
|
||||
|
||||
---
|
||||
|
||||
## Lists
|
||||
|
||||
Lists are ordered, mutable sequences of strings. All list operations start with the keyword `list`.
|
||||
|
||||
### Create or reset - `list … new`
|
||||
|
||||
```
|
||||
list <name> new
|
||||
```
|
||||
|
||||
Creates an empty list. If the list already exists it is cleared.
|
||||
|
||||
### Add an item - `list … add`
|
||||
|
||||
```
|
||||
list <name> add <value>
|
||||
```
|
||||
|
||||
Appends `<value>` to the end of the list.
|
||||
|
||||
### Get an item - `list … get`
|
||||
|
||||
```
|
||||
list <name> get <index>
|
||||
```
|
||||
|
||||
Pushes the item at zero-based `<index>` onto the output stack. Access it with `%out`.
|
||||
|
||||
```ynt
|
||||
list fruits new
|
||||
list fruits add apple
|
||||
list fruits add banana
|
||||
list fruits get 0
|
||||
var first = %out
|
||||
print_line ${first}
|
||||
```
|
||||
|
||||
### Set an item - `list … set`
|
||||
|
||||
```
|
||||
list <name> set <index> <value>
|
||||
```
|
||||
|
||||
Replaces the item at `<index>` with `<value>`.
|
||||
|
||||
### Remove an item - `list … remove`
|
||||
|
||||
```
|
||||
list <name> remove <index>
|
||||
```
|
||||
|
||||
Removes the item at `<index>`. Subsequent items shift down.
|
||||
|
||||
### Insert an item - `list … insert`
|
||||
|
||||
```
|
||||
list <name> insert <index> <value>
|
||||
```
|
||||
|
||||
Inserts `<value>` before position `<index>`.
|
||||
|
||||
### Get the length - `list … length`
|
||||
|
||||
```
|
||||
list <name> length
|
||||
```
|
||||
|
||||
Pushes the number of items onto the output stack.
|
||||
|
||||
```ynt
|
||||
list fruits length
|
||||
var n = %out
|
||||
print_line ${n} items
|
||||
```
|
||||
|
||||
### Clear all items - `list … clear`
|
||||
|
||||
```
|
||||
list <name> clear
|
||||
```
|
||||
|
||||
Removes all items but keeps the list alive.
|
||||
|
||||
### Delete a list - `list … delete`
|
||||
|
||||
```
|
||||
list <name> delete
|
||||
```
|
||||
|
||||
Removes the list entirely.
|
||||
|
||||
---
|
||||
|
||||
## Processing
|
||||
|
||||
### Arithmetic - `calc`
|
||||
|
||||
See [Arithmetic](#arithmetic).
|
||||
|
||||
### Decode a safe string - `eval`
|
||||
|
||||
```
|
||||
<expression> eval
|
||||
```
|
||||
|
||||
Decodes any internally-encoded characters in the current line back to their plain-text form.
|
||||
Useful after producing text from string literal operations that you want to re-use as plain text.
|
||||
|
||||
### Run from the current line in a background task - `task`
|
||||
|
||||
```
|
||||
<line> task
|
||||
```
|
||||
|
||||
Starts a background interpreter from the current line. In that background run, the current line is
|
||||
executed without the trailing `task`, and execution then continues through the remaining lines.
|
||||
The main script continues immediately. The task shares the global variable table with the main script.
|
||||
|
||||
```ynt
|
||||
print_line Starting background work... task
|
||||
print_line Main thread continues.
|
||||
```
|
||||
|
||||
#### Important behavior
|
||||
|
||||
- `task` does **not** run just one statement; it starts a second execution flow from that point onward.
|
||||
- Without careful control flow, lines after the `task` statement may run twice:
|
||||
once on the main thread and once in the background task.
|
||||
- Global variables are shared between both flows.
|
||||
|
||||
#### Recommended pattern
|
||||
|
||||
Use a direct function call with `task`, and terminate inside that function with `exit`.
|
||||
This keeps the worker logic isolated.
|
||||
|
||||
```ynt
|
||||
call background_job task
|
||||
print_line Main thread keeps going
|
||||
|
||||
func background_job:
|
||||
print_line Work in background
|
||||
exit
|
||||
```
|
||||
|
||||
This works well for task-only worker flows. In non-task/shared flows, prefer `return` if you want
|
||||
to return to the caller instead of terminating that execution flow with `exit`.
|
||||
|
||||
### Sleep - `sleep`
|
||||
|
||||
```
|
||||
sleep <milliseconds>
|
||||
```
|
||||
|
||||
Pauses execution for the given number of milliseconds. The argument must be a whole-number integer.
|
||||
Respects cancellation: if the script is
|
||||
stopped (e.g. via `abort_all` from another task), `sleep` returns early.
|
||||
|
||||
```ynt
|
||||
sleep 1000
|
||||
print_line One second later.
|
||||
```
|
||||
|
||||
### Get string length - `length`
|
||||
|
||||
```
|
||||
length <text>
|
||||
```
|
||||
|
||||
Pushes the character count of `<text>` onto the output stack.
|
||||
|
||||
```ynt
|
||||
length Hello, world!
|
||||
var len = %out
|
||||
print_line ${len}
|
||||
```
|
||||
|
||||
### Import another script - `import`
|
||||
|
||||
```
|
||||
import <path>
|
||||
```
|
||||
|
||||
Inlines the contents of another `.ynt` file at the current position.
|
||||
The extension `.ynt` is appended automatically if omitted.
|
||||
The path is resolved relative to the directory of the importing script.
|
||||
|
||||
```ynt
|
||||
import utils
|
||||
import lib/math.ynt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## System
|
||||
|
||||
### Execute a program - `exec`
|
||||
|
||||
```
|
||||
exec <program>
|
||||
exec <program> with <arg1>, <arg2>, …
|
||||
```
|
||||
|
||||
Runs an external program and waits for it to finish.
|
||||
Standard output and standard error are forwarded to the console in real time.
|
||||
The exit code and each line of output are pushed onto the output stack (exit code on top).
|
||||
|
||||
```ynt
|
||||
exec notepad
|
||||
exec git with status, --short
|
||||
var exit_code = %out
|
||||
print_line Exited with ${exit_code}
|
||||
```
|
||||
|
||||
You can also stage arguments with `push_in` before a bare `exec`:
|
||||
|
||||
```ynt
|
||||
push_in --version
|
||||
exec python
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Predefined tokens
|
||||
|
||||
These inline tokens are replaced with their value before the statement executes.
|
||||
|
||||
| Token | Replaced with |
|
||||
| ------- | --------------------------------------------------- |
|
||||
| `%time` | Current Unix timestamp (seconds) |
|
||||
| `%os` | Operating system platform name (e.g. `Win32NT`) |
|
||||
| `%cpu` | Processor architecture (e.g. `X64`) |
|
||||
| `%is64` | `True` if the OS is 64-bit, otherwise `False` |
|
||||
| `%pi` | The value of π |
|
||||
| `%rand` | A random integer in the range `[32767, 2147483647)` |
|
||||
|
||||
```ynt
|
||||
print_line Time: %time
|
||||
print_line OS: %os
|
||||
print_line PI: %pi
|
||||
var roll = %rand
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Termination
|
||||
|
||||
### Normal exit - `exit`
|
||||
|
||||
```
|
||||
exit
|
||||
```
|
||||
|
||||
Terminates the script gracefully. Background tasks that are still running are not cancelled.
|
||||
|
||||
### Exit and cancel all tasks - `abort_all`
|
||||
|
||||
```
|
||||
abort_all
|
||||
```
|
||||
|
||||
Terminates the script and signals all background tasks to stop.
|
||||
|
||||
### Throw an error - `throw`
|
||||
|
||||
```
|
||||
throw <message>
|
||||
```
|
||||
|
||||
Terminates the script and all tasks with `<message>` as the error description.
|
||||
A stack trace is printed showing the line and file where the error occurred.
|
||||
|
||||
```ynt
|
||||
var x = -1
|
||||
if ${x} < 0 call validate_fail
|
||||
|
||||
func validate_fail:
|
||||
throw x must not be negative
|
||||
return
|
||||
```
|
||||
|
||||
### Non-fatal error - `error`
|
||||
|
||||
```
|
||||
error <message>
|
||||
```
|
||||
|
||||
Like `throw` but does **not** cancel background tasks.
|
||||
|
||||
---
|
||||
|
||||
## Full statement reference
|
||||
|
||||
| Statement | Form | Description |
|
||||
| ------------------ | ------------------------------ | ------------------------------------------------------------ |
|
||||
| `var` | `var name = value` | Define/update local variable |
|
||||
| `global` | `global name = value` | Define/update global variable |
|
||||
| `delete` | `delete name` | Delete variable |
|
||||
| `print_line` | `print_line [text]` | Print line (optional text) |
|
||||
| `print` | `print text` | Print without newline |
|
||||
| `clear` | `clear` | Clear console |
|
||||
| `if … :` | `if cond:` | Start conditional block |
|
||||
| `else:` | `else:` | Else branch |
|
||||
| `end_if` | `end_if` | End conditional block |
|
||||
| `while … :` | `while cond:` | Start while loop |
|
||||
| `end_while` | `end_while` | End while loop |
|
||||
| `label` | `label name:` | Declare a jump target |
|
||||
| `goto` | `goto name` | Unconditional jump |
|
||||
| `if … goto` | `if cond goto name` | Conditional jump |
|
||||
| `func` | `func name:` | Declare a function |
|
||||
| `call` | `call name` | Call a function |
|
||||
| `call … with` | `call name with a, b` | Call a function with arguments |
|
||||
| `if … call` | `if cond call name` | Conditional function call |
|
||||
| `push_in` | `push_in value` | Push input argument |
|
||||
| `push_out` | `push_out value` | Push output value (inside function) |
|
||||
| `return` | `return` | Return from function |
|
||||
| `clear_call_stack` | `clear_call_stack` | Clear call stack |
|
||||
| `list … new` | `list name new` | Create/reset list |
|
||||
| `list … add` | `list name add value` | Append to list |
|
||||
| `list … get` | `list name get index` | Get item → `%out` |
|
||||
| `list … set` | `list name set index value` | Replace item |
|
||||
| `list … remove` | `list name remove index` | Remove item |
|
||||
| `list … insert` | `list name insert index value` | Insert item |
|
||||
| `list … length` | `list name length` | Get count → `%out` |
|
||||
| `list … clear` | `list name clear` | Clear all items |
|
||||
| `list … delete` | `list name delete` | Delete list |
|
||||
| `calc` | `expr calc` | Evaluate arithmetic |
|
||||
| `eval` | `expr eval` | Decode string encoding |
|
||||
| `task` | `line task` | Run current line (without `task`) and continue in background |
|
||||
| `sleep` | `sleep ms` | Sleep N milliseconds |
|
||||
| `length` | `length text` | Get char count → `%out` |
|
||||
| `import` | `import path` | Inline-include a `.ynt` file |
|
||||
| `exec` | `exec prog` | Run external program |
|
||||
| `exec … with` | `exec prog with a, b` | Run external program with args |
|
||||
| `exit` | `exit` | Graceful exit |
|
||||
| `abort_all` | `abort_all` | Exit and cancel all tasks |
|
||||
| `throw` | `throw message` | Fatal error (cancels all tasks) |
|
||||
| `error` | `error message` | Non-fatal error |
|
||||
| `%read_line` | inline | Insert one line of user input |
|
||||
| `%read_key` | inline | Insert one keypress |
|
||||
| `%in` | inline | Pop next function input argument |
|
||||
| `%has_in` | inline | `True`/`False` if input stack non-empty |
|
||||
| `%out` | inline | Pop top output value |
|
||||
| `%has_out` | inline | `True`/`False` if output stack non-empty |
|
||||
| `%time` | inline | Unix timestamp |
|
||||
| `%os` | inline | OS platform |
|
||||
| `%cpu` | inline | Processor architecture |
|
||||
| `%is64` | inline | `True` if 64-bit OS |
|
||||
| `%pi` | inline | Value of π |
|
||||
| `%rand` | inline | Random integer |
|
||||
@@ -0,0 +1,425 @@
|
||||
# YesNt Library API
|
||||
|
||||
The `YesNt.Interpreter` project is a .NET 8 class library. You can reference it from any C# project
|
||||
to embed the YesNt interpreter and run scripts programmatically.
|
||||
|
||||
---
|
||||
|
||||
## Table of contents
|
||||
|
||||
1. [Adding the reference](#adding-the-reference)
|
||||
2. [Running a script file](#running-a-script-file)
|
||||
3. [Running an in-memory script](#running-an-in-memory-script)
|
||||
4. [Capturing output (debug mode)](#capturing-output-debug-mode)
|
||||
5. [Adding custom statements](#adding-custom-statements)
|
||||
6. [Removing and disabling built-in statements](#removing-and-disabling-built-in-statements)
|
||||
7. [Stopping a script](#stopping-a-script)
|
||||
8. [Reading registered statements](#reading-registered-statements)
|
||||
9. [API reference](#api-reference)
|
||||
|
||||
---
|
||||
|
||||
## Adding the reference
|
||||
|
||||
Add a project reference to `YesNt.Interpreter` in your `.csproj`:
|
||||
|
||||
```xml
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\YesNt.Interpreter\YesNt.Interpreter.csproj" />
|
||||
</ItemGroup>
|
||||
```
|
||||
|
||||
Then add the using directive:
|
||||
|
||||
```csharp
|
||||
using YesNt.Interpreter.Runtime;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Running a script file
|
||||
|
||||
```csharp
|
||||
var interpreter = new YesNtInterpreter();
|
||||
interpreter.Execute("path/to/script.ynt");
|
||||
```
|
||||
|
||||
`Execute` is synchronous - it returns when the script finishes (or terminates with an error).
|
||||
|
||||
---
|
||||
|
||||
## Running an in-memory script
|
||||
|
||||
Supply a `List<string>` instead of a file path:
|
||||
|
||||
```csharp
|
||||
var lines = new List<string>
|
||||
{
|
||||
"var x = 42",
|
||||
"print_line The answer is ${x}",
|
||||
};
|
||||
|
||||
var interpreter = new YesNtInterpreter();
|
||||
interpreter.Execute(lines);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Capturing output (debug mode)
|
||||
|
||||
Pass `isDebugMode: true` to suppress direct console writes. Output is delivered through the
|
||||
`OnDebugOutput` event instead, and `OnLineExecuted` fires after executed lines.
|
||||
|
||||
```csharp
|
||||
var interpreter = new YesNtInterpreter();
|
||||
var output = new System.Text.StringBuilder();
|
||||
|
||||
interpreter.OnDebugOutput += text => output.Append(text);
|
||||
interpreter.OnLineExecuted += args =>
|
||||
{
|
||||
if (args is null)
|
||||
{
|
||||
// null means execution reached end-of-file (EOF)
|
||||
Console.WriteLine("Script reached EOF.");
|
||||
return;
|
||||
}
|
||||
Console.WriteLine($"Line {args.LineNumber}: {args.CurrentLine}");
|
||||
};
|
||||
|
||||
interpreter.Execute(new List<string> { "print_line hello" }, isDebugMode: true);
|
||||
Console.Write(output);
|
||||
```
|
||||
|
||||
`OnLineExecuted` receives a `DebugEventArgs` with:
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------------- | -------- | ------------------------------------------- |
|
||||
| `LineNumber` | `int` | 1-based line number |
|
||||
| `OriginalLine` | `string` | Raw source text |
|
||||
| `CurrentLine` | `string` | Text after all substitutions |
|
||||
| `IsTask` | `bool` | `true` if executed inside a background task |
|
||||
| `TaskId` | `int` | Task identifier (0 for the main thread) |
|
||||
|
||||
`OnLineExecuted` is invoked with `null` only when execution reaches end-of-file (EOF).
|
||||
If execution stops via `exit`, `throw`, `error`, or `Stop()`, no terminal `null` event is emitted.
|
||||
|
||||
---
|
||||
|
||||
## Adding custom statements
|
||||
|
||||
Use `AddStatement` to register keywords before calling `Execute`.
|
||||
|
||||
### Simple keyword at the start of a line
|
||||
|
||||
```csharp
|
||||
using YesNt.Interpreter.Enums;
|
||||
|
||||
var interpreter = new YesNtInterpreter();
|
||||
|
||||
interpreter.AddStatement("log", SearchMode.StartOfLine, SpaceAround.End, args =>
|
||||
{
|
||||
Console.WriteLine($"[LOG] {args}");
|
||||
});
|
||||
|
||||
interpreter.Execute(new List<string> { "log Hello from custom statement" });
|
||||
```
|
||||
|
||||
### Accessing script state from a handler
|
||||
|
||||
Pass an `Action<string, IStatementContext>` instead of `Action<string>` to receive the current
|
||||
script state. `IStatementContext` exposes the variable tables, the current line, the line number,
|
||||
and the ability to terminate execution.
|
||||
|
||||
```csharp
|
||||
using YesNt.Interpreter.Runtime;
|
||||
|
||||
interpreter.AddStatement("set_var", SearchMode.StartOfLine, SpaceAround.End,
|
||||
(args, ctx) =>
|
||||
{
|
||||
// args is e.g. "result 42" — parse however your syntax demands
|
||||
string[] parts = args.Split(' ', 2);
|
||||
if (parts.Length == 2)
|
||||
ctx.Variables[parts[0]] = parts[1];
|
||||
else
|
||||
ctx.Exit("set_var requires: <name> <value>", isError: true);
|
||||
});
|
||||
```
|
||||
|
||||
`IStatementContext` provides:
|
||||
|
||||
| Property | Type | Description |
|
||||
| ------------------------ | --------------------------- | -------------------------------------------------------------------------- |
|
||||
| `Variables` | `Dictionary<string,string>` | Local variable table for the current scope |
|
||||
| `GlobalVariables` | `Dictionary<string,string>` | Global variable table shared across all scopes |
|
||||
| `CurrentLine` | `string` | The line text being processed; write here for inline-substitution handlers |
|
||||
| `LineNumber` | `int` | Zero-based index of the _next_ line to execute; set to implement jumps |
|
||||
| `Exit(message, isError)` | `void` | Terminate execution; `isError: true` signals an error condition |
|
||||
|
||||
### With a syntax-highlight colour
|
||||
|
||||
```csharp
|
||||
interpreter.AddStatement("log", SearchMode.StartOfLine, SpaceAround.End,
|
||||
ConsoleColor.Cyan,
|
||||
args => Console.WriteLine($"[LOG] {args}"));
|
||||
```
|
||||
|
||||
### Using a pre-built `StatementAttribute`
|
||||
|
||||
```csharp
|
||||
using YesNt.Interpreter.Attributes;
|
||||
|
||||
var attr = new StatementAttribute("log", SearchMode.StartOfLine, SpaceAround.End)
|
||||
{
|
||||
Priority = Priority.VeryLow,
|
||||
};
|
||||
|
||||
interpreter.AddStatement(attr, args => Console.WriteLine($"[LOG] {args}"));
|
||||
```
|
||||
|
||||
### `SearchMode` values
|
||||
|
||||
| Value | The keyword matches when… |
|
||||
| ------------- | -------------------------------------------- |
|
||||
| `StartOfLine` | the line **starts with** the keyword |
|
||||
| `EndOfLine` | the line **ends with** the keyword |
|
||||
| `Contains` | the keyword appears **anywhere** in the line |
|
||||
| `Exact` | the line is **exactly** the keyword |
|
||||
|
||||
### `SpaceAround` values
|
||||
|
||||
| Value | Space requirement |
|
||||
| ---------- | -------------------------------- |
|
||||
| `None` | No surrounding spaces required |
|
||||
| `Start` | A space must precede the keyword |
|
||||
| `End` | A space must follow the keyword |
|
||||
| `StartEnd` | Spaces required on both sides |
|
||||
|
||||
Custom statements run at `Priority.Normal` by default. Statements with a higher-ranking enum member (`PreProcessing` → `Highest` → … → `VeryLow`) run first; `VeryLow` runs last.
|
||||
Use `StatementAttribute.Priority` to control ordering relative to built-in statements.
|
||||
|
||||
---
|
||||
|
||||
## Removing and disabling built-in statements
|
||||
|
||||
Use these methods to restrict which built-in keywords are available, useful for sandboxing
|
||||
or replacing a built-in with a custom implementation.
|
||||
|
||||
### `RemoveStatement` - permanent removal
|
||||
|
||||
Removes all handlers for the given keyword. Any script line that would have matched the
|
||||
keyword now triggers an **"Invalid statement"** error.
|
||||
|
||||
```csharp
|
||||
var interpreter = new YesNtInterpreter();
|
||||
|
||||
// Prevent scripts from launching external processes.
|
||||
interpreter.RemoveStatement("exec");
|
||||
|
||||
interpreter.Execute(new List<string> { "exec notepad" });
|
||||
// Terminates with: Invalid statement
|
||||
```
|
||||
|
||||
### `DisableStatement` - silent no-op
|
||||
|
||||
Disables all handlers for the keyword. The keyword still **matches** (so no error is raised),
|
||||
but has no effect. Use `EnableStatement` to restore the original behaviour.
|
||||
|
||||
```csharp
|
||||
var interpreter = new YesNtInterpreter();
|
||||
|
||||
// Make sleep a no-op so tests don't actually wait.
|
||||
interpreter.DisableStatement("sleep");
|
||||
|
||||
interpreter.Execute(new List<string>
|
||||
{
|
||||
"sleep 10000", // does nothing
|
||||
"var x = done",
|
||||
"print_line ${x}", // prints: done
|
||||
});
|
||||
```
|
||||
|
||||
### `EnableStatement` - restore a disabled statement
|
||||
|
||||
Restores the original handlers saved when `DisableStatement` was called.
|
||||
Has no effect if the statement is not currently disabled.
|
||||
|
||||
```csharp
|
||||
interpreter.DisableStatement("sleep");
|
||||
// ... configure other things ...
|
||||
interpreter.EnableStatement("sleep"); // sleep works normally again
|
||||
```
|
||||
|
||||
### Replacing a built-in statement
|
||||
|
||||
Call `RemoveStatement` to remove the built-in handlers, then `AddStatement` to install your own.
|
||||
Simply calling `AddStatement` with the same keyword name will **not** replace the built-in statement,
|
||||
it will add a second handler that fires alongside the original.
|
||||
|
||||
```csharp
|
||||
// Replace the built-in 'exec' with a sandboxed version that only allows 'echo'.
|
||||
interpreter.RemoveStatement("exec");
|
||||
interpreter.AddStatement("exec", SearchMode.StartOfLine, SpaceAround.End, args =>
|
||||
{
|
||||
if (args.Trim() != "echo")
|
||||
throw new InvalidOperationException("exec is restricted");
|
||||
|
||||
System.Diagnostics.Process.Start("cmd", "/c echo (sandboxed)");
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Stopping a script
|
||||
|
||||
Call `Stop()` from any thread to request graceful termination. The script stops at the next line
|
||||
boundary (or immediately if it is currently blocked waiting for console input).
|
||||
|
||||
```csharp
|
||||
var interpreter = new YesNtInterpreter();
|
||||
|
||||
// Start the script on a background thread so we can stop it from this thread.
|
||||
var thread = new System.Threading.Thread(() =>
|
||||
interpreter.Execute(new List<string> { "while True:", "sleep 100", "end_while" }));
|
||||
|
||||
thread.Start();
|
||||
System.Threading.Thread.Sleep(500);
|
||||
interpreter.Stop(); // signals the script to terminate at the next line boundary
|
||||
thread.Join();
|
||||
```
|
||||
|
||||
### Stopping a script that blocks on `%read_key`
|
||||
|
||||
When a script blocks waiting for keyboard input, use the `OnWaitingForInput` event instead of a
|
||||
fixed `Thread.Sleep`. The event fires at the exact moment the interpreter enters the blocking poll
|
||||
loop, so calling `Stop()` immediately after is always safe regardless of system load.
|
||||
|
||||
```csharp
|
||||
var interpreter = new YesNtInterpreter();
|
||||
var waitingForInput = new System.Threading.AutoResetEvent(false);
|
||||
|
||||
interpreter.OnWaitingForInput += () => waitingForInput.Set();
|
||||
|
||||
var thread = new System.Threading.Thread(() =>
|
||||
interpreter.Execute(new List<string> { "var key = %read_key" }));
|
||||
|
||||
thread.Start();
|
||||
waitingForInput.WaitOne(TimeSpan.FromSeconds(5)); // wait until blocked on input
|
||||
interpreter.Stop();
|
||||
thread.Join();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Reading registered statements
|
||||
|
||||
`StatementInformation` returns a read-only snapshot of every registered statement.
|
||||
This is useful for building syntax highlighters or tooling.
|
||||
|
||||
```csharp
|
||||
var interpreter = new YesNtInterpreter();
|
||||
|
||||
foreach (var info in interpreter.StatementInformation)
|
||||
{
|
||||
Console.WriteLine($"{info.Name,-20} {info.SearchMode,-12} color={info.Color}");
|
||||
}
|
||||
```
|
||||
|
||||
Each `StatementInformation` object exposes:
|
||||
|
||||
| Property | Type | Description |
|
||||
| -------------------------- | -------------- | ---------------------------- |
|
||||
| `Name` | `string` | The keyword |
|
||||
| `SearchMode` | `SearchMode` | Where the keyword is matched |
|
||||
| `SpaceAround` | `SpaceAround` | Required surrounding spaces |
|
||||
| `Color` | `ConsoleColor` | Syntax-highlight colour |
|
||||
| `IgnoreSyntaxHighlighting` | `bool` | Whether to skip highlighting |
|
||||
| `Separator` | `string?` | Optional required sub-string |
|
||||
|
||||
---
|
||||
|
||||
## API reference
|
||||
|
||||
### `YesNtInterpreter`
|
||||
|
||||
```csharp
|
||||
public class YesNtInterpreter
|
||||
```
|
||||
|
||||
#### Constructor
|
||||
|
||||
```csharp
|
||||
public YesNtInterpreter()
|
||||
```
|
||||
|
||||
Creates a new interpreter instance and registers all built-in statements.
|
||||
|
||||
#### Events
|
||||
|
||||
```csharp
|
||||
public event Action<string> OnDebugOutput;
|
||||
public event Action<DebugEventArgs> OnLineExecuted;
|
||||
public event Action OnWaitingForInput;
|
||||
```
|
||||
|
||||
`OnDebugOutput` and `OnLineExecuted` are only raised in debug mode (`isDebugMode: true`).
|
||||
`OnLineExecuted` receives `null` only on EOF completion.
|
||||
`OnWaitingForInput` is raised (in any mode) immediately before the interpreter blocks on
|
||||
`%read_key`. Use it to call `Stop()` deterministically without relying on `Thread.Sleep`.
|
||||
|
||||
#### Methods
|
||||
|
||||
```csharp
|
||||
// Execute a .ynt file
|
||||
public void Execute(string path, bool isDebugMode = false);
|
||||
|
||||
// Execute in-memory lines
|
||||
public void Execute(List<string> lines, bool isDebugMode = false);
|
||||
|
||||
// Register a custom statement (full control)
|
||||
public void AddStatement(StatementAttribute attribute, Action<string> handler);
|
||||
public void AddStatement(StatementAttribute attribute, Action<string, IStatementContext> handler);
|
||||
|
||||
// Register a custom statement (convenience overloads)
|
||||
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action<string> handler);
|
||||
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, Action<string, IStatementContext> handler);
|
||||
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor color, Action<string> handler);
|
||||
public void AddStatement(string name, SearchMode searchMode, SpaceAround spaceAround, ConsoleColor color, Action<string, IStatementContext> handler);
|
||||
|
||||
// Remove a built-in or custom statement permanently
|
||||
public void RemoveStatement(string name);
|
||||
|
||||
// Disable a statement (silent no-op; reversible)
|
||||
public void DisableStatement(string name);
|
||||
|
||||
// Re-enable a previously disabled statement
|
||||
public void EnableStatement(string name);
|
||||
|
||||
// Request graceful stop
|
||||
public void Stop();
|
||||
```
|
||||
|
||||
#### Properties
|
||||
|
||||
```csharp
|
||||
// Read-only snapshot of all registered statements
|
||||
public ReadOnlyCollection<StatementInformation> StatementInformation { get; }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `IStatementContext`
|
||||
|
||||
```csharp
|
||||
public interface IStatementContext // YesNt.Interpreter.Runtime
|
||||
```
|
||||
|
||||
Passed to `Action<string, IStatementContext>` handlers registered via `AddStatement`.
|
||||
Provides access to the script state that a built-in statement handler would have.
|
||||
|
||||
| Member | Type | Description |
|
||||
| ------------------------ | --------------------------- | ------------------------------------------------------------------------- |
|
||||
| `Variables` | `Dictionary<string,string>` | Local variable table for the current scope |
|
||||
| `GlobalVariables` | `Dictionary<string,string>` | Global variable table shared across all scopes |
|
||||
| `CurrentLine` | `string` | The line being processed; write here for inline-substitution handlers |
|
||||
| `LineNumber` | `int` | Zero-based index of the next line to execute; set this to implement jumps |
|
||||
| `Exit(message, isError)` | `void` | Terminate execution with a message; `isError: true` signals an error |
|
||||
Reference in New Issue
Block a user