mirror of
https://github.com/Stone-Red-Code/YesNt-Interpreter.git
synced 2026-09-05 23:42:45 +02:00
Add snapcraft & chocolatey manifests and move code to src
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
[*.cs]
|
||||
|
||||
# Default severity for analyzer diagnostics with category 'Critical Code Smell'
|
||||
dotnet_analyzer_diagnostic.category-Critical Code Smell.severity = none
|
||||
csharp_indent_labels = one_less_than_current
|
||||
csharp_using_directive_placement = outside_namespace:silent
|
||||
csharp_prefer_simple_using_statement = true:suggestion
|
||||
csharp_prefer_braces = true:silent
|
||||
csharp_style_namespace_declarations = file_scoped:silent
|
||||
csharp_style_expression_bodied_methods = false:silent
|
||||
csharp_style_expression_bodied_constructors = false:silent
|
||||
csharp_style_expression_bodied_operators = false:silent
|
||||
csharp_style_expression_bodied_properties = true:silent
|
||||
csharp_style_expression_bodied_indexers = true:silent
|
||||
csharp_style_expression_bodied_accessors = true:silent
|
||||
csharp_style_expression_bodied_lambdas = true:silent
|
||||
csharp_style_expression_bodied_local_functions = false:silent
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_prefer_null_check_over_type_check = true:suggestion
|
||||
csharp_prefer_simple_default_expression = true:suggestion
|
||||
csharp_style_prefer_local_over_anonymous_function = true:silent
|
||||
csharp_style_prefer_index_operator = true:suggestion
|
||||
csharp_style_prefer_range_operator = true:suggestion
|
||||
|
||||
[*.{cs,vb}]
|
||||
dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
tab_width = 4
|
||||
indent_size = 4
|
||||
end_of_line = crlf
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
||||
dotnet_style_prefer_auto_properties = true:silent
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_prefer_simplified_boolean_expressions = true:silent
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = true:silent
|
||||
dotnet_style_prefer_conditional_expression_over_return = true:silent
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||
dotnet_style_prefer_compound_assignment = true:suggestion
|
||||
dotnet_style_prefer_simplified_interpolation = true:suggestion
|
||||
dotnet_style_namespace_match_folder = true:suggestion
|
||||
[*.cs]
|
||||
#### Naming styles ####
|
||||
|
||||
# Naming rules
|
||||
|
||||
dotnet_naming_rule.const_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.const_should_be_pascal_case.symbols = const
|
||||
dotnet_naming_rule.const_should_be_pascal_case.style = pascal_case
|
||||
|
||||
# Symbol specifications
|
||||
|
||||
dotnet_naming_symbols.const.applicable_kinds =
|
||||
dotnet_naming_symbols.const.applicable_accessibilities = *
|
||||
dotnet_naming_symbols.const.required_modifiers = const
|
||||
|
||||
# Naming styles
|
||||
|
||||
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
|
||||
|
||||
# S1172: Unused method parameters should be removed
|
||||
dotnet_diagnostic.S1172.severity = none
|
||||
|
||||
[*.{cs,vb}]
|
||||
#### Naming styles ####
|
||||
|
||||
# Naming rules
|
||||
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
|
||||
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
|
||||
|
||||
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
|
||||
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
|
||||
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
|
||||
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
|
||||
|
||||
# Symbol specifications
|
||||
|
||||
dotnet_naming_symbols.interface.applicable_kinds = interface
|
||||
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.interface.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
|
||||
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.types.required_modifiers =
|
||||
|
||||
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
|
||||
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
|
||||
dotnet_naming_symbols.non_field_members.required_modifiers =
|
||||
|
||||
# Naming styles
|
||||
|
||||
dotnet_naming_style.begins_with_i.required_prefix = I
|
||||
dotnet_naming_style.begins_with_i.required_suffix =
|
||||
dotnet_naming_style.begins_with_i.word_separator =
|
||||
dotnet_naming_style.begins_with_i.capitalization = pascal_case
|
||||
|
||||
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
|
||||
|
||||
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,98 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31912.275
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YesNt.CodeEditor", "YesNt.CodeEditor\YesNt.CodeEditor.csproj", "{33BDA036-A37E-475E-AACF-8ED96E31BA1A}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "YesNt.Interpreter", "YesNt.Interpreter\YesNt.Interpreter.csproj", "{E40573DB-912A-4871-BB65-3EE6E7D72E79}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{52024699-4F8E-40F6-AF03-71DDB4A1DC06}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
..\.editorconfig = ..\.editorconfig
|
||||
EndProjectSection
|
||||
EndProject
|
||||
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
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {8597E655-80C0-48F2-AEB1-4A279F5173D6}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,402 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Timers;
|
||||
|
||||
using YesNt.Interpreter.Runtime;
|
||||
|
||||
namespace YesNt.CodeEditor;
|
||||
|
||||
internal class TextEditor
|
||||
{
|
||||
private readonly InputHandler inputHandler;
|
||||
private readonly SyntaxHighlighter syntaxHighlighter;
|
||||
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; } = [];
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
public TextEditor()
|
||||
{
|
||||
YesNtInterpreter.OnDebugOutput += YesNtInterpreter_OnDebugOutput;
|
||||
YesNtInterpreter.OnLineExecuted += YesNtInterpreter_OnLineExecuted;
|
||||
syntaxHighlighter = new(YesNtInterpreter.StatementInformation);
|
||||
inputHandler = new InputHandler(this);
|
||||
Console.CancelKeyPress += Console_CancelKeyPress;
|
||||
|
||||
Timer timer = new Timer(100);
|
||||
timer.Elapsed += (s, e) =>
|
||||
{
|
||||
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()
|
||||
{
|
||||
Console.Clear();
|
||||
|
||||
Display(true);
|
||||
do
|
||||
{
|
||||
Display(false);
|
||||
} while (inputHandler.HandleInput());
|
||||
|
||||
Console.Clear();
|
||||
}
|
||||
|
||||
public void Display(bool drawAll)
|
||||
{
|
||||
Console.CursorVisible = false;
|
||||
Console.ForegroundColor = ConsoleColor.Gray;
|
||||
Console.BackgroundColor = ConsoleColor.Black;
|
||||
|
||||
Console.SetCursorPosition(0, 0);
|
||||
|
||||
if (SizeChanged())
|
||||
{
|
||||
drawAll = true;
|
||||
InputHandler.WriteStatus(string.Empty);
|
||||
}
|
||||
|
||||
if (LineOffset < 0 || CursorPosition.Y < 0 || CursorPosition.Y < 0)
|
||||
{
|
||||
LineOffset = 0;
|
||||
CursorPosition.Y = 0;
|
||||
CursorPosition.X = 0;
|
||||
}
|
||||
|
||||
for (int i = LineOffset; i < Console.WindowHeight + LineOffset - 2; i++)
|
||||
{
|
||||
Console.SetCursorPosition(0, i - LineOffset);
|
||||
|
||||
string lineCountString = $"{i + 1}".PadRight(GetSpacing(), ' ') + "| ";
|
||||
if (i < Lines.Count)
|
||||
{
|
||||
if (CursorPosition.Y == i || drawAll)
|
||||
{
|
||||
Console.Write(lineCountString);
|
||||
string printLine = $"{Lines[i][..Math.Min(Lines[i].Length, Console.WindowWidth)]}".TrimEnd();
|
||||
syntaxHighlighter.Write(printLine);
|
||||
Console.Write(new string(' ', Math.Max(Console.WindowWidth - lineCountString.Length - printLine.Length, 0)));
|
||||
}
|
||||
}
|
||||
else if (drawAll || CursorPosition.Y == i)
|
||||
{
|
||||
Console.Write(lineCountString + new string(' ', Console.WindowWidth - lineCountString.Length));
|
||||
}
|
||||
}
|
||||
|
||||
Console.SetCursorPosition(0, Console.WindowHeight - 3);
|
||||
Console.Write(new string('-', Console.WindowWidth));
|
||||
Console.SetCursorPosition(0, Console.WindowHeight - 2);
|
||||
Console.Write(">>>" + new string(' ', Console.WindowWidth - 3));
|
||||
|
||||
Console.SetCursorPosition(Math.Min(CursorPosition.X + GetSpacing() + 2, Console.WindowWidth - 1), Math.Min(CursorPosition.Y - LineOffset, Console.WindowHeight - 4));
|
||||
|
||||
Console.CursorVisible = true;
|
||||
}
|
||||
|
||||
public bool Load(string path)
|
||||
{
|
||||
if (string.IsNullOrEmpty(Path.GetExtension(path)))
|
||||
{
|
||||
path = Path.ChangeExtension(path, "ynt");
|
||||
}
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
InputHandler.WriteStatus("File does not exist!");
|
||||
return false;
|
||||
}
|
||||
|
||||
Lines.Clear();
|
||||
Lines.AddRange(File.ReadAllLines(path));
|
||||
CurrentPath = path;
|
||||
InputHandler.WriteStatus("File Loaded!");
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool Save(string input, bool loadIfExists)
|
||||
{
|
||||
string path;
|
||||
if (input.Split(' ').Length == 2)
|
||||
{
|
||||
path = input.Split(' ')[1];
|
||||
|
||||
if (CurrentPath.Trim() != path.Trim() && loadIfExists)
|
||||
{
|
||||
return Load(path);
|
||||
}
|
||||
if (string.IsNullOrEmpty(Path.GetExtension(path)))
|
||||
{
|
||||
path = Path.ChangeExtension(path, "ynt");
|
||||
}
|
||||
CurrentPath = path;
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(CurrentPath))
|
||||
{
|
||||
path = CurrentPath;
|
||||
}
|
||||
else if (input.Split(' ').Length > 2)
|
||||
{
|
||||
InputHandler.WriteStatus("Invalid arguments!");
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
InputHandler.WriteStatus("File path is empty! (Save the file before you can use this command)");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(Path.GetExtension(path)))
|
||||
{
|
||||
path = Path.ChangeExtension(path, "ynt");
|
||||
}
|
||||
|
||||
while (Lines.Count > 0 && string.IsNullOrWhiteSpace(Lines[^1]))
|
||||
{
|
||||
Lines.RemoveAt(Lines.Count - 1);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllLines(path, Lines);
|
||||
InputHandler.WriteStatus("File Saved!");
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
InputHandler.WriteStatus(ex.Message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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(DebugEventArgs e)
|
||||
{
|
||||
lock (Console.Out)
|
||||
{
|
||||
if (e is not null)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Magenta;
|
||||
|
||||
string sharedString = (Console.CursorLeft != 0) ? Environment.NewLine : string.Empty;
|
||||
sharedString += (e.IsTask ? $"[Task: {e.TaskId}]" : string.Empty) + $"[{e.LineNumber}]";
|
||||
|
||||
if (e.OriginalLine == e.CurrentLine)
|
||||
{
|
||||
Console.WriteLine($"{sharedString}[{ToLiteral(e.CurrentLine)}] ==>");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"{sharedString}[{ToLiteral(e.OriginalLine)}] => [{ToLiteral(e.CurrentLine)}] ==>");
|
||||
}
|
||||
Console.ForegroundColor = ConsoleColor.Gray;
|
||||
}
|
||||
string[] outputs = debugOutput.ToArray();
|
||||
debugOutput.Clear();
|
||||
|
||||
foreach (string output in outputs)
|
||||
{
|
||||
Console.Write(output);
|
||||
}
|
||||
|
||||
if (IsStepDebugMode && e is not null && !e.IsTask)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.DarkGray;
|
||||
Console.WriteLine("[Step] Press any key for next line (Ctrl+C to stop)...");
|
||||
Console.ForegroundColor = ConsoleColor.Gray;
|
||||
_ = Console.ReadKey(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
|
||||
{
|
||||
e.Cancel = true;
|
||||
switch (EditMode)
|
||||
{
|
||||
case Mode.Debug:
|
||||
YesNtInterpreter.Stop();
|
||||
EditMode = Mode.Command;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private bool SizeChanged()
|
||||
{
|
||||
if (oldSize.X != Console.WindowWidth || oldSize.Y != Console.WindowHeight)
|
||||
{
|
||||
oldSize.X = Console.WindowWidth;
|
||||
oldSize.Y = Console.WindowHeight;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal class Point(int x, int y)
|
||||
{
|
||||
public int X { get; set; } = x;
|
||||
public int Y { get; set; } = y;
|
||||
}
|
||||
|
||||
internal enum Mode
|
||||
{
|
||||
Edit,
|
||||
Command,
|
||||
Debug
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// This file is used by Code Analysis to maintain SuppressMessage
|
||||
// attributes that are applied to this project.
|
||||
// Project-level suppressions either have no target or are given
|
||||
// a specific target and scoped to a namespace, type, member, etc.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
[assembly: SuppressMessage("Critical Code Smell", "S3998:Threads should not lock on objects with weak identity", Justification = "<Pending>", Scope = "member", Target = "~M:YesNt.CodeEditor.TextEditor.YesNtInterpreter_OnLineExecuted(YesNt.Interpreter.Runtime.DebugEventArgs)")]
|
||||
@@ -0,0 +1,390 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
|
||||
namespace YesNt.CodeEditor;
|
||||
|
||||
internal class InputHandler(TextEditor textEditor)
|
||||
{
|
||||
private readonly TextEditor textEditor = textEditor;
|
||||
|
||||
public bool HandleInput()
|
||||
{
|
||||
while (Console.KeyAvailable)
|
||||
{
|
||||
_ = Console.ReadKey(true);
|
||||
}
|
||||
if (textEditor.EditMode == Mode.Edit)
|
||||
{
|
||||
ConsoleKeyInfo keyInfo = Console.ReadKey(true);
|
||||
|
||||
if ((ConsoleModifiers.Alt & keyInfo.Modifiers) == ConsoleModifiers.Alt)
|
||||
{
|
||||
switch (keyInfo.Key)
|
||||
{
|
||||
case ConsoleKey.C:
|
||||
textEditor.EditMode = Mode.Command;
|
||||
return true;
|
||||
|
||||
case ConsoleKey.B:
|
||||
int position = textEditor.Lines.Count;
|
||||
textEditor.CursorPosition.Y = position - 1;
|
||||
textEditor.LineOffset = Math.Max(position - Console.WindowHeight + 3, 0);
|
||||
|
||||
textEditor.Display(true);
|
||||
return true;
|
||||
|
||||
case ConsoleKey.T:
|
||||
textEditor.CursorPosition.Y = 0;
|
||||
textEditor.LineOffset = 0;
|
||||
|
||||
textEditor.Display(true);
|
||||
return true;
|
||||
|
||||
case ConsoleKey.S:
|
||||
textEditor.CursorPosition.X = 0;
|
||||
return true;
|
||||
|
||||
case ConsoleKey.E:
|
||||
textEditor.CursorPosition.X = textEditor.Lines.Count > textEditor.CursorPosition.Y ? textEditor.Lines[textEditor.CursorPosition.Y].TrimEnd().Length : 0;
|
||||
return true;
|
||||
|
||||
case ConsoleKey.R:
|
||||
ExecuteWithDebugScreen("run", false, false);
|
||||
textEditor.Display(true);
|
||||
return true;
|
||||
|
||||
case ConsoleKey.D:
|
||||
ExecuteWithDebugScreen("debug", true, false);
|
||||
textEditor.Display(true);
|
||||
return true;
|
||||
|
||||
case ConsoleKey.F:
|
||||
textEditor.FormatLines();
|
||||
textEditor.Display(true);
|
||||
WriteStatus("Formatted!");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (keyInfo.Key == ConsoleKey.DownArrow)
|
||||
{
|
||||
textEditor.CursorPosition.Y++;
|
||||
if (textEditor.CursorPosition.Y - textEditor.LineOffset >= Console.WindowHeight - 3)
|
||||
{
|
||||
textEditor.LineOffset++;
|
||||
textEditor.Display(true);
|
||||
}
|
||||
}
|
||||
else if (keyInfo.Key == ConsoleKey.UpArrow)
|
||||
{
|
||||
if (textEditor.CursorPosition.Y > 0)
|
||||
{
|
||||
textEditor.CursorPosition.Y--;
|
||||
if (textEditor.CursorPosition.Y - textEditor.LineOffset < 0 && textEditor.LineOffset > 0)
|
||||
{
|
||||
textEditor.LineOffset--;
|
||||
textEditor.Display(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (keyInfo.Key == ConsoleKey.LeftArrow)
|
||||
{
|
||||
if (textEditor.CursorPosition.X > 0)
|
||||
{
|
||||
textEditor.CursorPosition.X--;
|
||||
}
|
||||
}
|
||||
else if (keyInfo.Key == ConsoleKey.RightArrow)
|
||||
{
|
||||
if (textEditor.CursorPosition.X + textEditor.GetSpacing() + 3 < Console.WindowWidth)
|
||||
{
|
||||
textEditor.CursorPosition.X++;
|
||||
}
|
||||
}
|
||||
else if (keyInfo.Key == ConsoleKey.Enter)
|
||||
{
|
||||
while (textEditor.Lines.Count <= textEditor.CursorPosition.Y)
|
||||
{
|
||||
textEditor.Lines.Add("");
|
||||
}
|
||||
textEditor.Lines.Insert(textEditor.CursorPosition.Y, "");
|
||||
|
||||
string line = textEditor.Lines[textEditor.CursorPosition.Y + 1];
|
||||
|
||||
textEditor.Lines[textEditor.CursorPosition.Y] = line[..Math.Min(textEditor.CursorPosition.X, line.Length)];
|
||||
textEditor.Lines[textEditor.CursorPosition.Y + 1] = line[Math.Min(textEditor.CursorPosition.X, line.Length)..];
|
||||
|
||||
textEditor.CursorPosition.X = 0;
|
||||
textEditor.CursorPosition.Y++;
|
||||
|
||||
if (textEditor.CursorPosition.Y - textEditor.LineOffset >= Console.WindowHeight - 3)
|
||||
{
|
||||
textEditor.LineOffset++;
|
||||
}
|
||||
textEditor.Display(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
while (textEditor.Lines.Count <= textEditor.CursorPosition.Y)
|
||||
{
|
||||
textEditor.Lines.Add("");
|
||||
}
|
||||
|
||||
StringBuilder lineBuilder = new StringBuilder(textEditor.Lines[textEditor.CursorPosition.Y]);
|
||||
while (lineBuilder.Length <= textEditor.CursorPosition.X)
|
||||
{
|
||||
_ = lineBuilder.Append(' ');
|
||||
}
|
||||
|
||||
textEditor.Lines[textEditor.CursorPosition.Y] = lineBuilder.ToString();
|
||||
|
||||
if (keyInfo.Key is ConsoleKey.Backspace or ConsoleKey.Delete)
|
||||
{
|
||||
if (keyInfo.Key == ConsoleKey.Delete)
|
||||
{
|
||||
textEditor.CursorPosition.X++;
|
||||
}
|
||||
|
||||
if (textEditor.CursorPosition.X > 0)
|
||||
{
|
||||
if (textEditor.Lines[textEditor.CursorPosition.Y][textEditor.CursorPosition.X - 1] == ' ' && textEditor.CursorPosition.X > textEditor.Lines[textEditor.CursorPosition.Y].TrimEnd().Length)
|
||||
{
|
||||
textEditor.Lines[textEditor.CursorPosition.Y] = textEditor.Lines[textEditor.CursorPosition.Y].TrimEnd();
|
||||
textEditor.CursorPosition.X = textEditor.Lines[textEditor.CursorPosition.Y].Length;
|
||||
}
|
||||
else
|
||||
{
|
||||
textEditor.Lines[textEditor.CursorPosition.Y] = textEditor.Lines[textEditor.CursorPosition.Y].Remove(textEditor.CursorPosition.X - 1, 1);
|
||||
textEditor.CursorPosition.X--;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (textEditor.CursorPosition.Y > 0)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(textEditor.Lines[textEditor.CursorPosition.Y - 1]))
|
||||
{
|
||||
textEditor.Lines.RemoveAt(--textEditor.CursorPosition.Y);
|
||||
}
|
||||
else
|
||||
{
|
||||
textEditor.CursorPosition.Y--;
|
||||
textEditor.CursorPosition.X = textEditor.Lines[textEditor.CursorPosition.Y].TrimEnd().Length;
|
||||
textEditor.Lines[textEditor.CursorPosition.Y] += textEditor.Lines[textEditor.CursorPosition.Y + 1];
|
||||
textEditor.Lines.RemoveAt(textEditor.CursorPosition.Y + 1);
|
||||
}
|
||||
if (textEditor.CursorPosition.Y - textEditor.LineOffset < 0 && textEditor.LineOffset > 0)
|
||||
{
|
||||
textEditor.LineOffset--;
|
||||
}
|
||||
textEditor.Display(true);
|
||||
}
|
||||
}
|
||||
|
||||
while (textEditor.Lines.Count > 0 && string.IsNullOrWhiteSpace(textEditor.Lines[^1]))
|
||||
{
|
||||
textEditor.Lines.RemoveAt(textEditor.Lines.Count - 1);
|
||||
}
|
||||
}
|
||||
else if (textEditor.CursorPosition.X + textEditor.GetSpacing() + 3 < Console.WindowWidth)
|
||||
{
|
||||
char input = keyInfo.KeyChar;
|
||||
if (!char.IsControl(input))
|
||||
{
|
||||
textEditor.Lines[textEditor.CursorPosition.Y] = textEditor.Lines[textEditor.CursorPosition.Y].Insert(textEditor.CursorPosition.X, input.ToString());
|
||||
textEditor.CursorPosition.X++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (textEditor.EditMode == Mode.Command)
|
||||
{
|
||||
Console.SetCursorPosition(3, Console.WindowHeight - 2);
|
||||
|
||||
string input = Console.ReadLine() ?? string.Empty;
|
||||
string command = input.Split(' ')[0].Trim();
|
||||
string path;
|
||||
|
||||
Console.CursorVisible = false;
|
||||
|
||||
switch (command)
|
||||
{
|
||||
case "edit":
|
||||
WriteStatus(string.Empty);
|
||||
textEditor.EditMode = Mode.Edit;
|
||||
break;
|
||||
|
||||
case "line":
|
||||
WriteStatus(string.Empty);
|
||||
|
||||
bool success = false;
|
||||
int lineNumber = 0;
|
||||
if (input.Split(' ').Length == 2)
|
||||
{
|
||||
success = int.TryParse(input.Split(' ')[1], out lineNumber);
|
||||
}
|
||||
|
||||
if (success && lineNumber > 0)
|
||||
{
|
||||
textEditor.CursorPosition.Y = lineNumber - 1;
|
||||
textEditor.CursorPosition.X = 0;
|
||||
textEditor.LineOffset = lineNumber - 1;
|
||||
textEditor.EditMode = Mode.Edit;
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteStatus("Invalid line number!");
|
||||
}
|
||||
break;
|
||||
|
||||
case "save":
|
||||
_ = textEditor.Save(input, false);
|
||||
break;
|
||||
|
||||
case "run":
|
||||
ExecuteWithDebugScreen(input, false, false);
|
||||
break;
|
||||
|
||||
case "debug":
|
||||
if (TryParseDebugCommand(input, out bool stepMode, out string parsedPath))
|
||||
{
|
||||
string saveInput = string.IsNullOrWhiteSpace(parsedPath) ? "debug" : $"debug {parsedPath}";
|
||||
ExecuteWithDebugScreen(saveInput, true, stepMode);
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteStatus("Invalid arguments!");
|
||||
}
|
||||
break;
|
||||
|
||||
case "load":
|
||||
if (input.Split(' ').Length == 2)
|
||||
{
|
||||
path = input.Split(' ')[1];
|
||||
}
|
||||
else
|
||||
{
|
||||
WriteStatus("Invalid arguments!");
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_ = textEditor.Load(path);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteStatus(ex.Message);
|
||||
}
|
||||
textEditor.LineOffset = 0;
|
||||
textEditor.CursorPosition.X = 0;
|
||||
textEditor.CursorPosition.Y = 0;
|
||||
break;
|
||||
|
||||
case "new":
|
||||
|
||||
textEditor.LineOffset = 0;
|
||||
textEditor.CursorPosition.X = 0;
|
||||
textEditor.CursorPosition.Y = 0;
|
||||
textEditor.CurrentPath = string.Empty;
|
||||
textEditor.Lines.Clear();
|
||||
WriteStatus(string.Empty);
|
||||
break;
|
||||
|
||||
case "format":
|
||||
textEditor.FormatLines();
|
||||
WriteStatus("Formatted!");
|
||||
break;
|
||||
|
||||
case "exit":
|
||||
return false;
|
||||
|
||||
default:
|
||||
WriteStatus("Command not found!");
|
||||
break;
|
||||
}
|
||||
textEditor.Display(true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void WriteStatus(string input)
|
||||
{
|
||||
Console.SetCursorPosition(0, Console.WindowHeight - 1);
|
||||
Console.Write(input + new string(' ', Console.WindowWidth - input.Length - 1));
|
||||
}
|
||||
|
||||
private static bool TryParseDebugCommand(string input, out bool stepMode, out string path)
|
||||
{
|
||||
stepMode = false;
|
||||
path = string.Empty;
|
||||
|
||||
string[] parts = input.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (parts.Length == 0 || !parts[0].Equals("debug", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
using YesNt.Interpreter.Enums;
|
||||
using YesNt.Interpreter.Runtime;
|
||||
using YesNt.Interpreter.Utilities;
|
||||
|
||||
namespace YesNt.CodeEditor;
|
||||
|
||||
internal partial class SyntaxHighlighter(ReadOnlyCollection<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.TrimStart(' ').StartsWith('#'))
|
||||
{
|
||||
input = AddColorInformation(input, input, ConsoleColor.Gray, SearchMode.Exact);
|
||||
}
|
||||
else
|
||||
{
|
||||
MatchCollection matches = StringRegex().Matches(input);
|
||||
for (int i = 0; i < matches.Count; i++)
|
||||
{
|
||||
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)
|
||||
{
|
||||
if (statement.IgnoreSyntaxHighlighting)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string name = statement.SpaceAround switch
|
||||
{
|
||||
SpaceAround.StartEnd => $" {statement.Name.Trim()} ",
|
||||
SpaceAround.Start => $" {statement.Name.Trim()}",
|
||||
SpaceAround.End => $"{statement.Name.Trim()} ",
|
||||
_ => statement.Name.Trim()
|
||||
};
|
||||
input = input.TrimEnd(' ');
|
||||
string inputTrim = input.Trim(' ');
|
||||
if (statement.SearchMode == SearchMode.StartOfLine && inputTrim.StartsWith(name))
|
||||
{
|
||||
if (statement.Separator is not null && input.Contains(statement.Separator))
|
||||
{
|
||||
input = AddColorInformation(input, statement.Separator, statement.Color, SearchMode.StartOfLine);
|
||||
}
|
||||
else if (statement.Separator is not null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
input = AddColorInformation(input, name, statement.Color, statement.SearchMode);
|
||||
}
|
||||
else if (statement.SearchMode == SearchMode.Contains && input.Contains(name))
|
||||
{
|
||||
if (statement.Separator is not null && input.Contains(statement.Separator))
|
||||
{
|
||||
input = AddColorInformation(input, statement.Separator, statement.Color, SearchMode.StartOfLine);
|
||||
}
|
||||
else if (statement.Separator is not null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
input = AddColorInformation(input, input.Substring(input.IndexOf(name), name.Length), statement.Color, statement.SearchMode);
|
||||
}
|
||||
else if (statement.SearchMode == SearchMode.EndOfLine && input.EndsWith(name))
|
||||
{
|
||||
if (statement.Separator is not null && input.Contains(statement.Separator))
|
||||
{
|
||||
input = AddColorInformation(input, statement.Separator, statement.Color, SearchMode.StartOfLine);
|
||||
}
|
||||
else if (statement.Separator is not null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
input = AddColorInformation(input, input[^name.Length..], statement.Color, statement.SearchMode);
|
||||
}
|
||||
else if (statement.SearchMode == SearchMode.Exact && inputTrim.Equals(name))
|
||||
{
|
||||
if (statement.Separator is not null && input.Contains(statement.Separator))
|
||||
{
|
||||
input = AddColorInformation(input, statement.Separator, statement.Color, SearchMode.StartOfLine);
|
||||
}
|
||||
else if (statement.Separator is not null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
input = AddColorInformation(input, input, statement.Color, statement.SearchMode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string part in input.Split("\0"))
|
||||
{
|
||||
ConsoleColor consoleColor;
|
||||
string messagePart = part;
|
||||
|
||||
string stringColor = StringColorRegex().Match(messagePart).Value;
|
||||
bool succ = int.TryParse(stringColor, out int colorIndex);
|
||||
if (succ && colorIndex >= 0 && colorIndex < 16)
|
||||
{
|
||||
messagePart = Base64Decode(messagePart.Replace("\x01" + stringColor + "\x01", string.Empty));
|
||||
consoleColor = (ConsoleColor)colorIndex;
|
||||
}
|
||||
else
|
||||
{
|
||||
consoleColor = ConsoleColor.White;
|
||||
}
|
||||
|
||||
if (consoleColor == Console.BackgroundColor)
|
||||
{
|
||||
consoleColor = ConsoleColor.White;
|
||||
}
|
||||
Console.ForegroundColor = consoleColor;
|
||||
Console.Write(messagePart);
|
||||
}
|
||||
Console.ForegroundColor = ConsoleColor.Gray;
|
||||
}
|
||||
|
||||
private static string AddColorInformation(string originalString, string value, ConsoleColor color, SearchMode searchMode)
|
||||
{
|
||||
int spacesAtEnd = value.WhiteSpaceAtEnd();
|
||||
|
||||
string base64Value = Base64Encode(value.TrimEnd());
|
||||
|
||||
string result = searchMode switch
|
||||
{
|
||||
SearchMode.StartOfLine => originalString.ReplaceFirstOccurrence(value, $"\0\x01{(int)color}\x01{base64Value}\0" + new string(' ', spacesAtEnd)),
|
||||
SearchMode.EndOfLine => originalString.ReplaceLastOccurrence(value, $"\0\x01{(int)color}\x01{base64Value}\0" + new string(' ', spacesAtEnd)),
|
||||
_ => originalString.Replace(value, $"\0\x01{(int)color}\x01{base64Value}\0" + new string(' ', spacesAtEnd))
|
||||
};
|
||||
return result;
|
||||
}
|
||||
|
||||
// This regex matches variables in the format ${variableName}, where variableName consists of alphanumeric characters.
|
||||
[GeneratedRegex("\\$\\{[a-zA-Z0-9]+\\}")]
|
||||
private static partial Regex VariableRegex();
|
||||
|
||||
// This regex matches string literals, taking into account escaped quotes and backslashes.
|
||||
[GeneratedRegex(@"(?<!\\)(?:\\\\{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();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<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>
|
||||
|
||||
</Project>
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
|
||||
<Platforms>AnyCPU;x64</Platforms>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<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>
|
||||
<ProjectReference Include="..\YesNt.Interpreter\YesNt.Interpreter.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,126 @@
|
||||
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;
|
||||
|
||||
namespace YesNt.Interpreter.Tests;
|
||||
|
||||
internal static class YesNtAssert
|
||||
{
|
||||
public static void IsLastLineEqual(List<string> lines, string expected, int timeout = 1000)
|
||||
{
|
||||
(DebugEventArgs? debugEventArgs, _) = ExecuteAndCapture(lines, timeout);
|
||||
Assert.AreEqual(expected, debugEventArgs?.CurrentLine);
|
||||
}
|
||||
|
||||
public static void IsLineEqual(string line, string expected, int timeout = 1000)
|
||||
{
|
||||
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();
|
||||
|
||||
yesNtInterpreter.OnLineExecuted += (er) =>
|
||||
{
|
||||
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));
|
||||
|
||||
return (debugEventArgs, outputBuilder.ToString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
using System;
|
||||
|
||||
using YesNt.Interpreter.Enums;
|
||||
|
||||
namespace YesNt.Interpreter.Attributes;
|
||||
|
||||
/// <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;
|
||||
SpaceAround = spaceAround;
|
||||
Color = color;
|
||||
}
|
||||
|
||||
/// <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;
|
||||
SpaceAround = spaceAround;
|
||||
IgnoreSyntaxHighlighting = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using System;
|
||||
|
||||
using YesNt.Interpreter.Enums;
|
||||
|
||||
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)]
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"profiles": {
|
||||
"YesNt-Interpreter": {
|
||||
"commandName": "Project",
|
||||
"commandLineArgs": "code.ynt"
|
||||
},
|
||||
"WSL": {
|
||||
"commandName": "WSL2",
|
||||
"environmentVariables": {},
|
||||
"distributionName": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
|
||||
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}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace YesNt.Interpreter.Runtime;
|
||||
|
||||
/// <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)
|
||||
{
|
||||
/// <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);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
namespace YesNt.Interpreter.Runtime;
|
||||
|
||||
/// <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;
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
using YesNt.Interpreter.Utilities;
|
||||
|
||||
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
|
||||
{
|
||||
public event Action<string> OnDebugOutput;
|
||||
|
||||
public event Action<DebugEventArgs> OnLineExecuted;
|
||||
|
||||
private event Action<string, bool> OnExit;
|
||||
|
||||
private static int internalTaskId = 0;
|
||||
private readonly Dictionary<string, string> topVariables = [];
|
||||
private readonly Dictionary<string, List<string>> topLists = [];
|
||||
|
||||
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; } = [];
|
||||
public string CurrentLine { get; set; } = string.Empty;
|
||||
public string SearchLabel { get; set; } = string.Empty;
|
||||
public string SearchFunction { get; set; } = string.Empty;
|
||||
public int LineNumber { get; set; } = 0;
|
||||
public bool Stop { get; private set; } = false;
|
||||
public bool StopAllTasks { get; private set; } = false;
|
||||
public bool IsDebugMode { get; set; } = false;
|
||||
public string WorkingDirectory { get; set; } = string.Empty;
|
||||
public bool IsTask => ParentRuntimeInformation is not null;
|
||||
public int TaskId { get => IsTask ? field : 0; private set; } = 0;
|
||||
public bool InternalIsInFunction { get; set; }
|
||||
|
||||
public bool IsInFunction
|
||||
{
|
||||
get => InternalIsInFunction || FunctionCallStack.Count > 0;
|
||||
set => InternalIsInFunction = value;
|
||||
}
|
||||
|
||||
public Dictionary<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 => FunctionCallStack.Count == 0 ? field : FunctionCallStack.Peek().Labels; } = [];
|
||||
|
||||
public RuntimeInformation ParentRuntimeInformation
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
field?.OnExit += ParentRuntimeInformation_OnExit;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSearching => !string.IsNullOrWhiteSpace(SearchLabel + SearchFunction) || (IsInFunction && FunctionCallStack.Count == 0);
|
||||
public bool IsLocalSearch { get; set; }
|
||||
|
||||
public void WriteLine(string output, bool forceWrite = false)
|
||||
{
|
||||
if ((Stop && !forceWrite) || (ParentRuntimeInformation?.StopAllTasks == true && !forceWrite))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsDebugMode)
|
||||
{
|
||||
if (IsTask)
|
||||
{
|
||||
ParentRuntimeInformation!.WriteLine(output.FromSafeString(), forceWrite);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnDebugOutput?.Invoke(output.FromSafeString() + Environment.NewLine);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine(output.FromSafeString());
|
||||
}
|
||||
}
|
||||
|
||||
public void Write(string output, bool forceWrite = false)
|
||||
{
|
||||
if ((Stop && !forceWrite) || (ParentRuntimeInformation?.StopAllTasks == true && !forceWrite))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsDebugMode)
|
||||
{
|
||||
if (IsTask)
|
||||
{
|
||||
ParentRuntimeInformation!.Write(output.FromSafeString(), forceWrite);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnDebugOutput?.Invoke(output.FromSafeString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Write(output.FromSafeString());
|
||||
}
|
||||
}
|
||||
|
||||
public void Exit(string message, bool stopAllTasks)
|
||||
{
|
||||
if (!Stop)
|
||||
{
|
||||
if (Lines.Count == 0)
|
||||
{
|
||||
WriteLine($"{Environment.NewLine}[{(IsTask ? $"Task {TaskId}" : "The process")} was terminated with the message: {message}]", true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Line line = Lines[Math.Min(LineNumber, Lines.Count - 1)];
|
||||
WriteLine($"{Environment.NewLine}[{(IsTask ? $"Task {TaskId}" : "The process")} was terminated at line {line.LineNumber + 1} in the file \"{line.FileName}\" with the message: {message}]", true);
|
||||
}
|
||||
|
||||
while (FunctionCallStack.Count > 0)
|
||||
{
|
||||
int stackLineNumber = FunctionCallStack.Pop().CallerLine;
|
||||
List<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;
|
||||
}
|
||||
if (stopAllTasks && !StopAllTasks)
|
||||
{
|
||||
StopAllTasks = true;
|
||||
OnExit?.Invoke(message, StopAllTasks);
|
||||
ParentRuntimeInformation?.Exit(ExitMessages.TerminatedByChildTask, true);
|
||||
}
|
||||
}
|
||||
|
||||
public void LineExecuted(DebugEventArgs debugEventArgs)
|
||||
{
|
||||
if (IsTask)
|
||||
{
|
||||
ParentRuntimeInformation.LineExecuted(debugEventArgs);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnLineExecuted?.Invoke(debugEventArgs);
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
topVariables.Clear();
|
||||
topLists.Clear();
|
||||
Lines.Clear();
|
||||
GlobalVariables.Clear();
|
||||
Labels.Clear();
|
||||
Functions.Clear();
|
||||
BlockBoundaries.Clear();
|
||||
FunctionCallStack.Clear();
|
||||
InParametersStack.Clear();
|
||||
OutParametersStack.Clear();
|
||||
ParentRuntimeInformation = null;
|
||||
SearchLabel = string.Empty;
|
||||
SearchFunction = string.Empty;
|
||||
WorkingDirectory = string.Empty;
|
||||
CurrentLine = string.Empty;
|
||||
Stop = false;
|
||||
StopAllTasks = false;
|
||||
IsDebugMode = false;
|
||||
IsInFunction = false;
|
||||
IsLocalSearch = false;
|
||||
LineNumber = 0;
|
||||
#pragma warning disable S2696 // internalTaskId is a shared counter intentionally incremented by each Reset call
|
||||
TaskId = Interlocked.Increment(ref internalTaskId);
|
||||
#pragma warning restore S2696
|
||||
}
|
||||
|
||||
private void ParentRuntimeInformation_OnExit(string exitMessage, bool stopAllTasks)
|
||||
{
|
||||
Exit(ExitMessages.TerminatedByParentTask, stopAllTasks);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
|
||||
using YesNt.Interpreter.Enums;
|
||||
|
||||
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; }
|
||||
|
||||
/// <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; }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,549 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
||||
using YesNt.Interpreter.Attributes;
|
||||
using YesNt.Interpreter.Enums;
|
||||
using YesNt.Interpreter.Utilities;
|
||||
|
||||
namespace YesNt.Interpreter.Runtime;
|
||||
|
||||
/// <summary>
|
||||
/// The main entry point for executing YesNt scripts.
|
||||
/// </summary>
|
||||
public class YesNtInterpreter
|
||||
{
|
||||
/// <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> information = statements.Select(s =>
|
||||
{
|
||||
return new StatementInformation()
|
||||
{
|
||||
Name = s.Key.Name,
|
||||
SearchMode = s.Key.SearchMode,
|
||||
SpaceAround = s.Key.SpaceAround,
|
||||
Color = s.Key.Color,
|
||||
IgnoreSyntaxHighlighting = s.Key.IgnoreSyntaxHighlighting,
|
||||
Separator = s.Key.Separator
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
return new ReadOnlyCollection<StatementInformation>(information);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new <see cref="YesNtInterpreter"/> and registers all built-in statements.
|
||||
/// </summary>
|
||||
public YesNtInterpreter()
|
||||
{
|
||||
GeneratedStatementRegistry.Register(runtimeInfo, out statements, out staticStatements);
|
||||
UpdateStatementHandlers();
|
||||
runtimeInfo.PreScanLinesAction = PreScanLines;
|
||||
|
||||
runtimeInfo.OnDebugOutput += (s) => OnDebugOutput?.Invoke(s);
|
||||
runtimeInfo.OnLineExecuted += e => OnLineExecuted?.Invoke(e);
|
||||
}
|
||||
|
||||
private void UpdateStatementHandlers()
|
||||
{
|
||||
statementHandlers = statements.Select(s =>
|
||||
{
|
||||
string name = s.Key.SpaceAround switch
|
||||
{
|
||||
SpaceAround.StartEnd => $" {s.Key.Name.Trim()} ",
|
||||
SpaceAround.Start => $" {s.Key.Name.Trim()}",
|
||||
SpaceAround.End => $"{s.Key.Name.Trim()} ",
|
||||
_ => s.Key.Name.Trim()
|
||||
};
|
||||
return new StatementHandler(s.Key, s.Value, name);
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
/// <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;
|
||||
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();
|
||||
runtimeInfo.IsDebugMode = isDebugMode;
|
||||
|
||||
for (int i = 0; i < lines.Count; i++)
|
||||
{
|
||||
string content = lines[i].Trim().Replace("\r", string.Empty);
|
||||
runtimeInfo.Lines.Add(new Line(content, Path.GetFileName("#Memory#"), i));
|
||||
}
|
||||
|
||||
PreScanLines();
|
||||
Execute();
|
||||
}
|
||||
|
||||
internal void Execute(List<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.GlobalVariables = globalVariables;
|
||||
if (parentRuntimeInformation.StopAllTasks)
|
||||
{
|
||||
runtimeInfo.Exit(ExitMessages.TerminatedByParentTask, parentRuntimeInformation.StopAllTasks);
|
||||
return;
|
||||
}
|
||||
PreScanLines();
|
||||
Execute();
|
||||
}
|
||||
|
||||
private void Execute()
|
||||
{
|
||||
for (; runtimeInfo.LineNumber < runtimeInfo.Lines.Count; runtimeInfo.LineNumber++)
|
||||
{
|
||||
if (runtimeInfo.Stop)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Line lineObj = runtimeInfo.Lines[runtimeInfo.LineNumber];
|
||||
runtimeInfo.CurrentLine = lineObj.Content;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(runtimeInfo.CurrentLine) || runtimeInfo.CurrentLine.StartsWith('#'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DebugEventArgs debugEventArgs = null;
|
||||
if (runtimeInfo.IsDebugMode)
|
||||
{
|
||||
debugEventArgs = new DebugEventArgs()
|
||||
{
|
||||
LineNumber = runtimeInfo.LineNumber + 1,
|
||||
OriginalLine = runtimeInfo.CurrentLine.FromSafeString(),
|
||||
IsTask = runtimeInfo.IsTask,
|
||||
TaskId = runtimeInfo.TaskId
|
||||
};
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<StaticStatementAttribute, Action> staticStatement in staticStatements)
|
||||
{
|
||||
StaticStatementAttribute staticStatementAttribute = staticStatement.Key;
|
||||
if (!staticStatementAttribute.ExecuteInSearchMode && runtimeInfo.IsSearching)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
staticStatement.Value.Invoke();
|
||||
}
|
||||
|
||||
bool statementFound = false;
|
||||
bool notSearchingLabel = !runtimeInfo.IsSearching;
|
||||
|
||||
List<StatementHandler> handlers = (runtimeInfo.LineNumber < lineMatchingHandlers.Count) ? lineMatchingHandlers[runtimeInfo.LineNumber] : [];
|
||||
|
||||
foreach (StatementHandler handler in handlers)
|
||||
{
|
||||
StatementAttribute statementAttribute = handler.Attribute;
|
||||
|
||||
if (!statementAttribute.ExecuteInSearchMode && runtimeInfo.IsSearching)
|
||||
{
|
||||
statementFound = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (runtimeInfo.Stop)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
string name = handler.FullName;
|
||||
|
||||
if (statementAttribute.Separator is null || runtimeInfo.CurrentLine.Contains(statementAttribute.Separator, StringComparison.Ordinal))
|
||||
{
|
||||
if (statementAttribute.SearchMode == SearchMode.StartOfLine && runtimeInfo.CurrentLine.StartsWith(name, StringComparison.Ordinal))
|
||||
{
|
||||
string copyLine = statementAttribute.KeepStatementInArgs ? runtimeInfo.CurrentLine : runtimeInfo.CurrentLine[name.Length..];
|
||||
handler.Handler.Invoke(copyLine);
|
||||
statementFound = true;
|
||||
}
|
||||
else if (statementAttribute.SearchMode == SearchMode.Contains && runtimeInfo.CurrentLine.Contains(name, StringComparison.Ordinal))
|
||||
{
|
||||
string copyLine = statementAttribute.KeepStatementInArgs ? runtimeInfo.CurrentLine : runtimeInfo.CurrentLine.Replace(name, string.Empty);
|
||||
handler.Handler.Invoke(copyLine);
|
||||
statementFound = true;
|
||||
}
|
||||
else if (statementAttribute.SearchMode == SearchMode.EndOfLine && runtimeInfo.CurrentLine.EndsWith(name, StringComparison.Ordinal))
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!statementFound)
|
||||
{
|
||||
runtimeInfo.Exit(ExitMessages.InvalidStatement, true);
|
||||
}
|
||||
if (runtimeInfo.IsDebugMode && notSearchingLabel && debugEventArgs != null)
|
||||
{
|
||||
debugEventArgs.CurrentLine = runtimeInfo.CurrentLine.FromSafeString();
|
||||
runtimeInfo.LineExecuted(debugEventArgs);
|
||||
}
|
||||
}
|
||||
|
||||
if (!runtimeInfo.Stop)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(runtimeInfo.SearchLabel))
|
||||
{
|
||||
runtimeInfo.Exit(ExitMessages.LabelNotFound(runtimeInfo.SearchLabel), true);
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(runtimeInfo.SearchFunction))
|
||||
{
|
||||
runtimeInfo.Exit(ExitMessages.FunctionNotFound(runtimeInfo.SearchFunction), true);
|
||||
}
|
||||
else
|
||||
{
|
||||
runtimeInfo.Exit(ExitMessages.EndOfFile, false);
|
||||
}
|
||||
|
||||
if (runtimeInfo.IsDebugMode)
|
||||
{
|
||||
runtimeInfo.LineExecuted(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool LoadFile(string path)
|
||||
{
|
||||
path = Path.GetFullPath(path);
|
||||
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string[] lines = File.ReadAllLines(path);
|
||||
|
||||
runtimeInfo.WorkingDirectory = Path.GetDirectoryName(path);
|
||||
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
string content = lines[i].Trim().Replace("\r", string.Empty);
|
||||
runtimeInfo.Lines.Add(new Line(content, Path.GetFileName(path), i));
|
||||
}
|
||||
|
||||
PreScanLines();
|
||||
return true;
|
||||
}
|
||||
|
||||
internal void PreScanLines()
|
||||
{
|
||||
runtimeInfo.BlockBoundaries.Clear();
|
||||
lineMatchingHandlers = new List<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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
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;
|
||||
|
||||
internal class CodeFlowStatements : StatementRuntimeInformation
|
||||
{
|
||||
[Statement("goto", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow)]
|
||||
public void Jump(string args)
|
||||
{
|
||||
string key = NormalizeBlockName(args);
|
||||
|
||||
if (RuntimeInfo.Labels.TryGetValue(key, out int value))
|
||||
{
|
||||
RuntimeInfo.LineNumber = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.SearchLabel = key;
|
||||
RuntimeInfo.IsLocalSearch = RuntimeInfo.IsInFunction;
|
||||
}
|
||||
}
|
||||
|
||||
[Statement("if", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow, Separator = " goto ")]
|
||||
public void JumpIf(string args)
|
||||
{
|
||||
string[] parts = args.Split(" goto ", 2, StringSplitOptions.None);
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
|
||||
return;
|
||||
}
|
||||
|
||||
string condition = parts[0].Trim();
|
||||
string key = NormalizeBlockName(parts[1]);
|
||||
|
||||
bool? result = Evaluator.EvaluateCondition(condition);
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidOperation, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (RuntimeInfo.Labels.TryGetValue(key, out int value))
|
||||
{
|
||||
RuntimeInfo.LineNumber = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.SearchLabel = key;
|
||||
RuntimeInfo.IsLocalSearch = RuntimeInfo.IsInFunction;
|
||||
}
|
||||
}
|
||||
|
||||
[Statement("label", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, ExecuteInSearchMode = true, Separator = ":")]
|
||||
public void FindLabel(string args)
|
||||
{
|
||||
string labelDeclaration = args.Trim();
|
||||
if (!labelDeclaration.EndsWith(':'))
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidSyntaxColonRequired, true);
|
||||
return;
|
||||
}
|
||||
|
||||
string key = NormalizeBlockName(labelDeclaration);
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
|
||||
return;
|
||||
}
|
||||
|
||||
RuntimeInfo.Labels[key] = RuntimeInfo.LineNumber;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(RuntimeInfo.SearchLabel) && RuntimeInfo.SearchLabel == key)
|
||||
{
|
||||
RuntimeInfo.SearchLabel = string.Empty;
|
||||
RuntimeInfo.IsLocalSearch = false;
|
||||
}
|
||||
}
|
||||
|
||||
[Statement("call", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, Priority = Priority.VeryLow)]
|
||||
public void Call(string args)
|
||||
{
|
||||
CallFunction(NormalizeBlockName(args));
|
||||
}
|
||||
|
||||
[Statement("if", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, Priority = Priority.VeryLow, Separator = " call ")]
|
||||
public void CallIf(string args)
|
||||
{
|
||||
string[] parts = args.Split(" call ", 2, StringSplitOptions.None);
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
|
||||
return;
|
||||
}
|
||||
|
||||
string condition = parts[0].Trim();
|
||||
bool? result = Evaluator.EvaluateCondition(condition);
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidOperation, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
CallFunction(NormalizeBlockName(parts[1]));
|
||||
}
|
||||
}
|
||||
|
||||
[Statement("if", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow, Separator = ":", BlockPair = "end_if")]
|
||||
public void IfBlock(string args)
|
||||
{
|
||||
args = args.Trim();
|
||||
if (!args.EndsWith(':'))
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidSyntaxColonRequired, true);
|
||||
return;
|
||||
}
|
||||
|
||||
string condition = args[..^1].Trim();
|
||||
bool? result = Evaluator.EvaluateCondition(condition);
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidOperation, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int targetLine = FindBlockBoundary(RuntimeInfo.LineNumber);
|
||||
if (targetLine < 0)
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.NoMatchingEndIf, true);
|
||||
return;
|
||||
}
|
||||
|
||||
RuntimeInfo.LineNumber = targetLine;
|
||||
}
|
||||
|
||||
[Statement("else:", SearchMode.Exact, SpaceAround.None, ConsoleColor.Green, IsBlockIntermediate = true, BlockPair = "end_if")]
|
||||
public void Else(string _)
|
||||
{
|
||||
int targetLine = FindBlockBoundary(RuntimeInfo.LineNumber);
|
||||
if (targetLine < 0)
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.NoMatchingEndIf, true);
|
||||
return;
|
||||
}
|
||||
|
||||
RuntimeInfo.LineNumber = targetLine;
|
||||
}
|
||||
|
||||
[Statement("end_if", SearchMode.Exact, SpaceAround.None, ConsoleColor.Green, IsBlockEnd = true)]
|
||||
public void EndIf(string _)
|
||||
{
|
||||
// Intentionally empty: end_if is a block-boundary marker only; no runtime action needed.
|
||||
}
|
||||
|
||||
[Statement("while", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Green, Priority = Priority.VeryLow, Separator = ":", BlockPair = "end_while")]
|
||||
public void While(string args)
|
||||
{
|
||||
args = args.Trim();
|
||||
if (!args.EndsWith(':'))
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidSyntaxColonRequired, true);
|
||||
return;
|
||||
}
|
||||
|
||||
string condition = args[..^1].Trim();
|
||||
bool? result = Evaluator.EvaluateCondition(condition);
|
||||
|
||||
if (result is null)
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidOperation, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (result == true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int endWhileLine = FindBlockBoundary(RuntimeInfo.LineNumber);
|
||||
if (endWhileLine < 0)
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.NoMatchingEndWhile, true);
|
||||
return;
|
||||
}
|
||||
|
||||
RuntimeInfo.LineNumber = endWhileLine;
|
||||
}
|
||||
|
||||
[Statement("end_while", SearchMode.Exact, SpaceAround.None, ConsoleColor.Green, IsBlockEnd = true)]
|
||||
public void EndWhile(string _)
|
||||
{
|
||||
int whileLine = FindBlockBoundary(RuntimeInfo.LineNumber);
|
||||
if (whileLine < 0)
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.NoMatchingWhile, true);
|
||||
return;
|
||||
}
|
||||
|
||||
RuntimeInfo.LineNumber = whileLine - 1;
|
||||
}
|
||||
|
||||
[Statement("exit", SearchMode.Exact, SpaceAround.None, ConsoleColor.Red, ExecuteInSearchMode = true)]
|
||||
public void End(string _)
|
||||
{
|
||||
HandleExit(ExitMessages.PlannedTermination, false);
|
||||
}
|
||||
|
||||
[Statement("abort_all", SearchMode.Exact, SpaceAround.None, ConsoleColor.Red, ExecuteInSearchMode = true)]
|
||||
public void Terminate(string _)
|
||||
{
|
||||
HandleExit(ExitMessages.PlannedTerminationCancelingTasks, true);
|
||||
}
|
||||
|
||||
[Statement("throw", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Red)]
|
||||
public void Throw(string message)
|
||||
{
|
||||
RuntimeInfo.Exit(message, true);
|
||||
}
|
||||
|
||||
[Statement("error", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Red)]
|
||||
public void Error(string message)
|
||||
{
|
||||
RuntimeInfo.Exit(message, false);
|
||||
}
|
||||
|
||||
private void CallFunction(string key)
|
||||
{
|
||||
RuntimeInfo.FunctionCallStack.Push(new FunctionScope(RuntimeInfo.LineNumber, new Stack<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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
using YesNt.Interpreter.Attributes;
|
||||
using YesNt.Interpreter.Enums;
|
||||
using YesNt.Interpreter.Runtime;
|
||||
using YesNt.Interpreter.Utilities;
|
||||
|
||||
namespace YesNt.Interpreter.Statements;
|
||||
|
||||
internal class ConsoleStatements : StatementRuntimeInformation
|
||||
{
|
||||
[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("print", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkGreen, Priority = Priority.VeryLow)]
|
||||
public void Write(string args)
|
||||
{
|
||||
RuntimeInfo.Write(args);
|
||||
}
|
||||
|
||||
[Statement("%read_line", SearchMode.Contains, SpaceAround.None, ConsoleColor.DarkGreen, KeepStatementInArgs = true, Priority = Priority.Highest)]
|
||||
public void ReadLine(string args)
|
||||
{
|
||||
args += " ";
|
||||
while (args.Contains("%read_line"))
|
||||
{
|
||||
string input = Console.ReadLine();
|
||||
if (input is null)
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.TerminatedByExternalProcess, true);
|
||||
return;
|
||||
}
|
||||
args = args.ReplaceFirstOccurrence("%read_line ", input.ToSafeString() + " ");
|
||||
}
|
||||
RuntimeInfo.CurrentLine = args.TrimEnd();
|
||||
}
|
||||
|
||||
[Statement("%read_key", SearchMode.Contains, SpaceAround.None, ConsoleColor.DarkGreen, KeepStatementInArgs = true, Priority = Priority.Highest)]
|
||||
public void ReadKey(string args)
|
||||
{
|
||||
args += " ";
|
||||
while (args.Contains("%read_key"))
|
||||
{
|
||||
string input = ConsoleExtensions.ReadKey(RuntimeInfo).ToString();
|
||||
args = args.ReplaceFirstOccurrence("%read_key ", input.ToSafeString() + " ");
|
||||
}
|
||||
RuntimeInfo.CurrentLine = args.TrimEnd();
|
||||
}
|
||||
|
||||
[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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
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;
|
||||
|
||||
internal class FunctionStatements : StatementRuntimeInformation
|
||||
{
|
||||
[Statement("func", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, ExecuteInSearchMode = true, Separator = ":")]
|
||||
public void FindFunction(string args)
|
||||
{
|
||||
if (RuntimeInfo.InternalIsInFunction)
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.NestedFunctionsNotAllowed, true);
|
||||
return;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(RuntimeInfo.SearchFunction) && RuntimeInfo.SearchFunction == key)
|
||||
{
|
||||
RuntimeInfo.SearchFunction = string.Empty;
|
||||
}
|
||||
|
||||
RuntimeInfo.IsInFunction = true;
|
||||
}
|
||||
|
||||
[Statement("push_in", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Yellow)]
|
||||
public void AddInParameter(string args)
|
||||
{
|
||||
RuntimeInfo.InParametersStack.Push(args);
|
||||
}
|
||||
|
||||
[Statement("%out", SearchMode.Contains, SpaceAround.None, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)]
|
||||
public void GetOutParameter(string args)
|
||||
{
|
||||
RuntimeInfo.CurrentLine = TemplateProcessor.ProcessStackParameters(args, "%out", RuntimeInfo.OutParametersStack, RuntimeInfo, ExitMessages.NoOutArgumentInStack);
|
||||
}
|
||||
|
||||
[Statement("%has_out", SearchMode.Contains, SpaceAround.None, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)]
|
||||
public void CheckIfOutParameterAvailable(string args)
|
||||
{
|
||||
args = args.Replace("%has_out", (RuntimeInfo.OutParametersStack.Count > 0).ToString());
|
||||
|
||||
RuntimeInfo.CurrentLine = args.TrimEnd();
|
||||
}
|
||||
|
||||
[Statement("call", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkYellow, Priority = Priority.Low, Separator = " with ")]
|
||||
public void Call(string args)
|
||||
{
|
||||
string[] parts = args.Split(" with ", 2, StringSplitOptions.None);
|
||||
if (parts.Length != 2)
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
|
||||
return;
|
||||
}
|
||||
|
||||
string key = NormalizeBlockName(parts[0]);
|
||||
string[] functionArguments = parts[1].Split(',');
|
||||
|
||||
foreach (string argument in functionArguments)
|
||||
{
|
||||
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.TryGetValue(key, out int value))
|
||||
{
|
||||
RuntimeInfo.LineNumber = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.SearchFunction = key;
|
||||
}
|
||||
}
|
||||
|
||||
[Statement("%in", SearchMode.Contains, SpaceAround.None, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)]
|
||||
public void GetInParameter(string args)
|
||||
{
|
||||
if (!RuntimeInfo.IsInFunction)
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.StatementNotAllowedOutsideFunction, true);
|
||||
return;
|
||||
}
|
||||
|
||||
RuntimeInfo.CurrentLine = TemplateProcessor.ProcessStackParameters(args, "%in", RuntimeInfo.FunctionCallStack.Peek().Arguments, RuntimeInfo, ExitMessages.NoInArgumentInStack);
|
||||
}
|
||||
|
||||
[Statement("%has_in", SearchMode.Contains, SpaceAround.None, ConsoleColor.Yellow, KeepStatementInArgs = true, Priority = Priority.Highest)]
|
||||
public void CheckIfInParameterAvailable(string args)
|
||||
{
|
||||
if (!RuntimeInfo.IsInFunction)
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.StatementNotAllowedOutsideFunction, true);
|
||||
return;
|
||||
}
|
||||
|
||||
args = args.Replace("%has_in", (RuntimeInfo.FunctionCallStack.Peek().Arguments.Count > 0).ToString());
|
||||
|
||||
RuntimeInfo.CurrentLine = args.TrimEnd();
|
||||
}
|
||||
|
||||
[Statement("push_out", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Yellow)]
|
||||
public void AddOutParameter(string args)
|
||||
{
|
||||
if (!RuntimeInfo.IsInFunction)
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.StatementNotAllowedOutsideFunction, true);
|
||||
return;
|
||||
}
|
||||
|
||||
RuntimeInfo.FunctionCallStack.Peek().Results.Push(args);
|
||||
}
|
||||
|
||||
[Statement("return", SearchMode.Exact, SpaceAround.None, ConsoleColor.DarkYellow, ExecuteInSearchMode = true)]
|
||||
public void Return(string _)
|
||||
{
|
||||
if (!RuntimeInfo.IsInFunction)
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.StatementNotAllowedOutsideFunction, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (RuntimeInfo.IsSearching)
|
||||
{
|
||||
RuntimeInfo.IsInFunction = false;
|
||||
|
||||
if (RuntimeInfo.IsLocalSearch)
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.LabelNotFound(RuntimeInfo.SearchLabel), true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
RuntimeInfo.IsInFunction = false;
|
||||
|
||||
if (RuntimeInfo.FunctionCallStack.Count > 0)
|
||||
{
|
||||
FunctionScope functionScope = RuntimeInfo.FunctionCallStack.Pop();
|
||||
|
||||
RuntimeInfo.OutParametersStack = new Stack<string>(functionScope.Results);
|
||||
RuntimeInfo.LineNumber = functionScope.CallerLine;
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.NoFunctionInStack, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Statement("clear_call_stack", SearchMode.Exact, SpaceAround.None, ConsoleColor.Red)]
|
||||
public void ClearCallStack(string _)
|
||||
{
|
||||
RuntimeInfo.FunctionCallStack.Clear();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using YesNt.Interpreter.Attributes;
|
||||
using YesNt.Interpreter.Enums;
|
||||
using YesNt.Interpreter.Runtime;
|
||||
using YesNt.Interpreter.Utilities;
|
||||
|
||||
namespace YesNt.Interpreter.Statements;
|
||||
|
||||
internal partial class ProcessingStatements : StatementRuntimeInformation
|
||||
{
|
||||
[Statement("calc", SearchMode.EndOfLine, SpaceAround.Start, ConsoleColor.DarkYellow, Priority = Priority.High)]
|
||||
public void Calculate(string args)
|
||||
{
|
||||
RuntimeInfo.CurrentLine = TemplateProcessor.ProcessCalculations(args.FromSafeString(), RuntimeInfo, CalculationRegex());
|
||||
}
|
||||
|
||||
[Statement("eval", SearchMode.EndOfLine, SpaceAround.Start, ConsoleColor.DarkYellow, Priority = Priority.VeryHigh)]
|
||||
public void Evaluate(string args)
|
||||
{
|
||||
RuntimeInfo.CurrentLine = args.FromSafeString();
|
||||
}
|
||||
|
||||
[Statement("task", SearchMode.EndOfLine, SpaceAround.Start, ConsoleColor.DarkYellow, Priority = Priority.VeryHigh)]
|
||||
public void RunTask(string line)
|
||||
{
|
||||
int lineNumber = RuntimeInfo.LineNumber;
|
||||
List<Line> lines = RuntimeInfo.Lines.GetRange(0, RuntimeInfo.Lines.Count);
|
||||
|
||||
Line oldLine = lines[lineNumber];
|
||||
|
||||
lines[lineNumber] = new Line(line, oldLine.FileName, oldLine.LineNumber);
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
YesNtInterpreter interpreter = new YesNtInterpreter();
|
||||
interpreter.Execute(lines, RuntimeInfo.GlobalVariables, lineNumber, RuntimeInfo);
|
||||
});
|
||||
|
||||
RuntimeInfo.CurrentLine = string.Empty;
|
||||
}
|
||||
|
||||
[Statement("sleep", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Magenta)]
|
||||
public void Sleep(string args)
|
||||
{
|
||||
if (int.TryParse(args, out int millisecondsTimeout))
|
||||
{
|
||||
ConsoleExtensions.Sleep(millisecondsTimeout, RuntimeInfo);
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidTimeoutValue(args), true);
|
||||
}
|
||||
}
|
||||
|
||||
[Statement("length", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Magenta)]
|
||||
public void Length(string args)
|
||||
{
|
||||
RuntimeInfo.InParametersStack.Clear();
|
||||
RuntimeInfo.OutParametersStack.Clear();
|
||||
|
||||
RuntimeInfo.OutParametersStack.Push(args.FromSafeString().Length.ToString());
|
||||
}
|
||||
|
||||
[Statement("import", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.Magenta)]
|
||||
public void Import(string path)
|
||||
{
|
||||
path = path.FromSafeString();
|
||||
path = Path.Combine(RuntimeInfo.WorkingDirectory, path);
|
||||
|
||||
if (string.IsNullOrEmpty(Path.GetExtension(path)))
|
||||
{
|
||||
path = Path.ChangeExtension(path, "ynt");
|
||||
}
|
||||
|
||||
if (File.Exists(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
RuntimeInfo.Lines.RemoveAt(RuntimeInfo.LineNumber);
|
||||
string[] lines = File.ReadAllLines(path);
|
||||
|
||||
for (int i = 0; i < lines.Length; i++)
|
||||
{
|
||||
RuntimeInfo.Lines.Insert(RuntimeInfo.LineNumber + i, new Line(lines[i], Path.GetFileName(path), i));
|
||||
}
|
||||
RuntimeInfo.PreScanLinesAction?.Invoke();
|
||||
RuntimeInfo.LineNumber--;
|
||||
}
|
||||
catch
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.CouldNotLoadFile(path), true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.CouldNotFindFile(path), true);
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex("[0-9*+().,^%/-]+[0-9*+ ().,^%/-]+[0-9*+().,^%/-]+")]
|
||||
private static partial Regex CalculationRegex();
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
using YesNt.Interpreter.Attributes;
|
||||
using YesNt.Interpreter.Enums;
|
||||
using YesNt.Interpreter.Runtime;
|
||||
using YesNt.Interpreter.Utilities;
|
||||
|
||||
namespace YesNt.Interpreter.Statements;
|
||||
|
||||
internal partial class VariableStatements : StatementRuntimeInformation
|
||||
{
|
||||
[Statement("var", SearchMode.StartOfLine, SpaceAround.End, System.ConsoleColor.DarkBlue, Priority = Priority.VeryLow, Separator = "=")]
|
||||
public void DefineVariable(string args)
|
||||
{
|
||||
DefineVariableIn(RuntimeInfo.Variables, args);
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
string key = parts[0].Trim();
|
||||
if (key.Contains(' '))
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
|
||||
}
|
||||
|
||||
dict[key] = parts[1].Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true);
|
||||
}
|
||||
}
|
||||
|
||||
[Statement("delete", SearchMode.StartOfLine, SpaceAround.End, System.ConsoleColor.Red, Priority = Priority.VeryLow)]
|
||||
public void DeleteVariable(string args)
|
||||
{
|
||||
string key = args.Trim();
|
||||
|
||||
if (RuntimeInfo.Variables.ContainsKey(key))
|
||||
{
|
||||
_ = RuntimeInfo.Variables.Remove(key);
|
||||
}
|
||||
else if (RuntimeInfo.GlobalVariables.ContainsKey(key))
|
||||
{
|
||||
_ = RuntimeInfo.GlobalVariables.Remove(key);
|
||||
}
|
||||
else
|
||||
{
|
||||
RuntimeInfo.Exit(ExitMessages.VariableNotFound(key), true);
|
||||
}
|
||||
}
|
||||
|
||||
[Statement("${", SearchMode.Contains, SpaceAround.None, Priority = Priority.Highest, Separator = "}")]
|
||||
public void ReadVariable(string _)
|
||||
{
|
||||
RuntimeInfo.CurrentLine = TemplateProcessor.ProcessVariables(RuntimeInfo.CurrentLine, RuntimeInfo);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
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)
|
||||
{
|
||||
input = input.FromSafeString();
|
||||
string lower = input.ToLower().Trim();
|
||||
if (lower == "true")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
else if (lower == "false")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string[] parts = input.Split("==");
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
string part1 = parts[0].Trim();
|
||||
string part2 = parts[1].Trim();
|
||||
return part1 == part2;
|
||||
}
|
||||
|
||||
parts = input.Split("!=");
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
string part1 = parts[0].Trim();
|
||||
string part2 = parts[1].Trim();
|
||||
return part1 != part2;
|
||||
}
|
||||
|
||||
parts = input.Split(">=");
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
bool succ1 = parts[0].ToStandardizedNumber(out double part1);
|
||||
bool succ2 = parts[1].ToStandardizedNumber(out double part2);
|
||||
return succ1 && succ2 && part1 >= part2;
|
||||
}
|
||||
|
||||
parts = input.Split("<=");
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
bool succ1 = parts[0].ToStandardizedNumber(out double part1);
|
||||
bool succ2 = parts[1].ToStandardizedNumber(out double part2);
|
||||
return succ1 && succ2 && part1 <= part2;
|
||||
}
|
||||
|
||||
parts = input.Split(">");
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
bool succ1 = parts[0].ToStandardizedNumber(out double part1);
|
||||
bool succ2 = parts[1].ToStandardizedNumber(out double part2);
|
||||
return succ1 && succ2 && part1 > part2;
|
||||
}
|
||||
|
||||
parts = input.Split("<");
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
bool succ1 = parts[0].ToStandardizedNumber(out double part1);
|
||||
bool succ2 = parts[1].ToStandardizedNumber(out double part2);
|
||||
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 = input.FromSafeString();
|
||||
input = PlusPlusRegex().Replace(input, "+");
|
||||
input = MinusMinusRegex().Replace(input, "+");
|
||||
input = MinusPlusRegex().Replace(input, "-");
|
||||
input = PlusMinusRegex().Replace(input, "-");
|
||||
|
||||
return CalculateInternal(input, '+');
|
||||
}
|
||||
|
||||
private static string CalculateInternal(string input, char op)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (input.ToStandardizedNumber(out double quickNum))
|
||||
{
|
||||
return quickNum.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
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 = CalculateInternal(calc, '+');
|
||||
input = input.Replace(matches[i].Value, ret);
|
||||
}
|
||||
matches = ParenthesesRegex().Matches(input);
|
||||
}
|
||||
|
||||
string[] parts = input.Split(op);
|
||||
|
||||
// If the expression starts with the operator (e.g. "-3 + 5" split by '-' gives ["", "3 + 5"]),
|
||||
// prepend the operator back onto the first real part so it isn't lost.
|
||||
if (parts.Length >= 2 && string.IsNullOrWhiteSpace(parts[0]))
|
||||
{
|
||||
parts[1] = $"{op}{parts[1]}";
|
||||
parts = parts.Skip(1).ToArray();
|
||||
}
|
||||
|
||||
double number = double.NaN;
|
||||
|
||||
foreach (string p in parts)
|
||||
{
|
||||
string part = p;
|
||||
|
||||
part = op switch
|
||||
{
|
||||
'+' => CalculateInternal(part, '-'),
|
||||
'-' => CalculateInternal(part, '*'),
|
||||
'*' => CalculateInternal(part, '/'),
|
||||
'/' => CalculateInternal(part, '%'),
|
||||
'%' => CalculateInternal(part, '^'),
|
||||
_ => part
|
||||
};
|
||||
|
||||
if (part is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (part.ToStandardizedNumber(out double num))
|
||||
{
|
||||
if (double.IsNaN(number))
|
||||
{
|
||||
number = num;
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (op)
|
||||
{
|
||||
case '+':
|
||||
number += num;
|
||||
break;
|
||||
|
||||
case '-':
|
||||
number -= num;
|
||||
break;
|
||||
|
||||
case '*':
|
||||
number *= num;
|
||||
break;
|
||||
|
||||
case '/':
|
||||
number /= num;
|
||||
break;
|
||||
|
||||
case '%':
|
||||
number %= num;
|
||||
break;
|
||||
|
||||
case '^':
|
||||
number = Math.Pow(number, num);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return number.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
[GeneratedRegex("\\(([^()]+)\\)")]
|
||||
private static partial Regex ParenthesesRegex();
|
||||
|
||||
[GeneratedRegex("(\\+ +\\+)+")]
|
||||
private static partial Regex PlusPlusRegex();
|
||||
|
||||
[GeneratedRegex("(\\- +\\-)+")]
|
||||
private static partial Regex MinusMinusRegex();
|
||||
|
||||
[GeneratedRegex("(\\- +\\+)+")]
|
||||
private static partial Regex MinusPlusRegex();
|
||||
|
||||
[GeneratedRegex("(\\+ +\\-)+")]
|
||||
private static partial Regex PlusMinusRegex();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<RootNamespace>YesNt.Interpreter</RootNamespace>
|
||||
<ApplicationIcon />
|
||||
<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>
|
||||
Reference in New Issue
Block a user