Add snapcraft & chocolatey manifests and move code to src

This commit is contained in:
Stone_Red
2026-03-06 00:51:31 +01:00
parent 5efd698772
commit 2a03352169
64 changed files with 302 additions and 5 deletions
+119
View File
@@ -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
+98
View File
@@ -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
+402
View File
@@ -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)")]
+390
View File
@@ -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;
}
}
+4
View File
@@ -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": ""
}
}
}
+176
View File
@@ -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>
+20
View File
@@ -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>
+126
View File
@@ -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;
}
+28
View File
@@ -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
}
+19
View File
@@ -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);
}
+16
View File
@@ -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 &gt; 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>