From 7d645f3c6e5255b666f8b180f701847cc0f08a30 Mon Sep 17 00:00:00 2001 From: Stone_Red <56473591+Stone-Red-Code@users.noreply.github.com> Date: Fri, 7 Aug 2026 23:36:02 +0200 Subject: [PATCH] Add support for maps --- docs/language-reference.md | 110 ++++++- .../MapStatementsTests.cs | 269 ++++++++++++++++++ src/YesNt.Interpreter/Runtime/ExitMessages.cs | 10 + .../Runtime/FunctionScope.cs | 3 + .../Runtime/RuntimeInformation.cs | 3 + .../Statements/MapStatements.cs | 222 +++++++++++++++ 6 files changed, 613 insertions(+), 4 deletions(-) create mode 100644 src/YesNt.Interpreter.Tests/MapStatementsTests.cs create mode 100644 src/YesNt.Interpreter/Statements/MapStatements.cs diff --git a/docs/language-reference.md b/docs/language-reference.md index 23e3d1d..d8c4063 100644 --- a/docs/language-reference.md +++ b/docs/language-reference.md @@ -17,10 +17,11 @@ Execution proceeds top-to-bottom unless a control-flow statement changes the lin 8. [Control flow](#control-flow) 9. [Functions](#functions) 10. [Lists](#lists) -11. [Processing](#processing) -12. [System](#system) -13. [Predefined tokens](#predefined-tokens) -14. [Termination](#termination) +11. [Maps](#maps) +12. [Processing](#processing) +13. [System](#system) +14. [Predefined tokens](#predefined-tokens) +15. [Termination](#termination) --- @@ -609,6 +610,99 @@ Removes the list entirely. --- +## Maps + +Maps are key–value stores. Each key is unique; putting a value under an existing key overwrites it. All map operations start with the keyword `map`. + +### Create or reset - `map … new` + +``` +map new +``` + +Creates an empty map. If the map already exists it is cleared. + +### Add or update an entry - `map … put` + +``` +map put , +``` + +Sets `` to ``. If the key already exists its value is replaced. The statement takes exactly two comma-separated parts. Keys may contain spaces unquoted. A key or value that itself contains a comma must be wrapped in quotes. + +```ynt +map settings new +map settings put name, Alice +map settings put city, Berlin +map settings get name +var who = %out +print_line ${who} +``` + +### Get a value - `map … get` + +``` +map get +``` + +Pushes the value stored under `` onto the output stack. Access it with `%out`. Terminates with an error if the key does not exist. + +### Check a key - `map … has` + +``` +map has +``` + +Pushes `True` if `` exists, `False` otherwise, onto the output stack. + +```ynt +map settings new +map settings put name Alice +map settings has name +var present = %out +print_line ${present} +``` + +### Remove an entry - `map … remove` + +``` +map remove +``` + +Removes `` and its value. Removing a key that does not exist is silently ignored. + +### Get the size - `map … size` + +``` +map size +``` + +Pushes the number of entries onto the output stack. + +```ynt +map settings size +var n = %out +print_line ${n} entries +``` + +### Clear all entries - `map … clear` + +``` +map clear +``` + +Removes all entries but keeps the map alive. + +### Delete a map - `map … delete` + +``` +map delete +``` + +Removes the map entirely. + +--- + ## Processing ### Arithmetic - `calc` @@ -842,6 +936,14 @@ Like `throw` but does **not** cancel background tasks. | `list … length` | `list name length` | Get count → `%out` | | `list … clear` | `list name clear` | Clear all items | | `list … delete` | `list name delete` | Delete list | +| `map … new` | `map name new` | Create/reset map | +| `map … put` | `map name put key, value` | Add or update entry | +| `map … get` | `map name get key` | Get value → `%out` | +| `map … has` | `map name has key` | `True`/`False` if key exists → `%out` | +| `map … remove` | `map name remove key` | Remove entry | +| `map … size` | `map name size` | Get entry count → `%out` | +| `map … clear` | `map name clear` | Clear all entries | +| `map … delete` | `map name delete` | Delete map | | `calc` | `expr calc` | Evaluate arithmetic | | `eval` | `expr eval` | Decode string encoding | | `task` | `line task` | Run current line (without `task`) and continue in background | diff --git a/src/YesNt.Interpreter.Tests/MapStatementsTests.cs b/src/YesNt.Interpreter.Tests/MapStatementsTests.cs new file mode 100644 index 0000000..5208df4 --- /dev/null +++ b/src/YesNt.Interpreter.Tests/MapStatementsTests.cs @@ -0,0 +1,269 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using System.Collections.Generic; + +namespace YesNt.Interpreter.Tests; + +[TestClass] +public class MapStatementsTests +{ + [TestMethod] + public void MapCreatePutGetTest() + { + List lines = + [ + "map settings new", + "map settings put a, 1", + "map settings put b, 2", + "map settings get b", + "var result = %out", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "2"); + } + + [TestMethod] + public void MapPutOverwritesTest() + { + List lines = + [ + "map settings new", + "map settings put a, 1", + "map settings put a, 2", + "map settings get a", + "var result = %out", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "2"); + } + + [TestMethod] + public void MapHasTest() + { + List lines = + [ + "map settings new", + "map settings put a, 1", + "map settings has a", + "var hasA = %out", + "map settings has b", + "var hasB = %out", + "${hasA} ${hasB}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "True False"); + } + + [TestMethod] + public void MapRemoveTest() + { + List lines = + [ + "map settings new", + "map settings put a, 1", + "map settings put b, 2", + "map settings remove a", + "map settings size", + "var n = %out", + "map settings has a", + "var hasA = %out", + "${n} ${hasA}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "1 False"); + } + + [TestMethod] + public void MapRemoveMissingKeyNoOpTest() + { + List lines = + [ + "map settings new", + "map settings put a, 1", + "map settings remove b", + "map settings size", + "var n = %out", + "${n}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "1"); + } + + [TestMethod] + public void MapSizeTest() + { + List lines = + [ + "map settings new", + "map settings put a, 1", + "map settings put b, 2", + "map settings size", + "var n = %out", + "${n}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "2"); + } + + [TestMethod] + public void MapClearTest() + { + List lines = + [ + "map settings new", + "map settings put a, 1", + "map settings clear", + "map settings size", + "var n = %out", + "${n}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "0"); + } + + [TestMethod] + public void MapDeleteTest() + { + List lines = + [ + "map settings new", + "map settings put a, 1", + "map settings delete", + "map settings size" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Map \"settings\" not found"); + } + + [TestMethod] + public void MapMissingFailsTest() + { + List lines = + [ + "map missing size" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Map \"missing\" not found"); + } + + [TestMethod] + public void MapGetMissingKeyFailsTest() + { + List lines = + [ + "map settings new", + "map settings get a" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Key \"a\" not found"); + } + + [TestMethod] + public void MapInvalidSyntaxFailsTest() + { + List lines = + [ + "map settings put" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Invalid statement"); + } + + [TestMethod] + public void MapPutExtraCommaFailsTest() + { + List lines = + [ + "map settings new", + "map settings put test, abc, htuerthieu" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Invalid syntax"); + } + + [TestMethod] + public void MapScopeInsideFunctionTest() + { + List lines = + [ + "goto main", + "func make:", + "map data new", + "map data put k, v", + "map data get k", + "push_out %out", + "return", + "label main:", + "call make", + "var result = %out", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "v"); + } + + [TestMethod] + public void MapDoesNotLeakToCallerTest() + { + List lines = + [ + "goto main", + "func make:", + "map data new", + "map data put k, v", + "return", + "label main:", + "call make", + "map data size" + ]; + + YesNtAssert.ContainsTerminationMessage(lines, "Map \"data\" not found"); + } + + [TestMethod] + public void MapPutValueWithSpacesTest() + { + List lines = + [ + "map settings new", + "map settings put \"display name\", \"John Doe\"", + "map settings get \"display name\"", + "var result = %out eval", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "John Doe"); + } + + [TestMethod] + public void MapKeyWithSpacesTest() + { + List lines = + [ + "map settings new", + "map settings put first name, John", + "map settings get first name", + "var result = %out", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "John"); + } + + [TestMethod] + public void MapValueWithCommaTest() + { + List lines = + [ + "map settings new", + "map settings put name, \"Doe, John\"", + "map settings get name", + "var result = %out eval", + "${result}" + ]; + + YesNtAssert.IsLastLineEqual(lines, "Doe, John"); + } +} diff --git a/src/YesNt.Interpreter/Runtime/ExitMessages.cs b/src/YesNt.Interpreter/Runtime/ExitMessages.cs index ab88025..1f1da74 100644 --- a/src/YesNt.Interpreter/Runtime/ExitMessages.cs +++ b/src/YesNt.Interpreter/Runtime/ExitMessages.cs @@ -51,6 +51,16 @@ internal static class ExitMessages return $"List \"{list}\" not found"; } + internal static string MapNotFound(string map) + { + return $"Map \"{map}\" not found"; + } + + internal static string KeyNotFound(string key) + { + return $"Key \"{key}\" not found"; + } + internal static string InvalidIndex(string rawIndex) { return $"\"{rawIndex}\" is not a valid index"; diff --git a/src/YesNt.Interpreter/Runtime/FunctionScope.cs b/src/YesNt.Interpreter/Runtime/FunctionScope.cs index 8203b46..0748768 100644 --- a/src/YesNt.Interpreter/Runtime/FunctionScope.cs +++ b/src/YesNt.Interpreter/Runtime/FunctionScope.cs @@ -17,6 +17,9 @@ internal class FunctionScope(int callerLine, Stack arguments) /// Gets the local list table for this function invocation. public Dictionary> Lists { get; } = []; + /// Gets the local map table for this function invocation. + public Dictionary> Maps { get; } = []; + /// Gets the local label table for this function invocation. public Dictionary Labels { get; } = []; diff --git a/src/YesNt.Interpreter/Runtime/RuntimeInformation.cs b/src/YesNt.Interpreter/Runtime/RuntimeInformation.cs index 1cfc9ab..c058f79 100644 --- a/src/YesNt.Interpreter/Runtime/RuntimeInformation.cs +++ b/src/YesNt.Interpreter/Runtime/RuntimeInformation.cs @@ -24,6 +24,7 @@ internal sealed class RuntimeInformation : IStatementContext private static int internalTaskId = 0; private readonly Dictionary topVariables = []; private readonly Dictionary> topLists = []; + private readonly Dictionary> topMaps = []; public Dictionary GlobalVariables { get; set; } = []; public Dictionary Functions { get; } = []; @@ -54,6 +55,7 @@ internal sealed class RuntimeInformation : IStatementContext public Dictionary Variables => FunctionCallStack.Count == 0 ? topVariables : FunctionCallStack.Peek().Variables; public Dictionary> Lists => FunctionCallStack.Count == 0 ? topLists : FunctionCallStack.Peek().Lists; + public Dictionary> Maps => FunctionCallStack.Count == 0 ? topMaps : FunctionCallStack.Peek().Maps; public Dictionary Labels { get => FunctionCallStack.Count == 0 ? field : FunctionCallStack.Peek().Labels; } = []; @@ -173,6 +175,7 @@ internal sealed class RuntimeInformation : IStatementContext { topVariables.Clear(); topLists.Clear(); + topMaps.Clear(); Lines.Clear(); GlobalVariables.Clear(); Labels.Clear(); diff --git a/src/YesNt.Interpreter/Statements/MapStatements.cs b/src/YesNt.Interpreter/Statements/MapStatements.cs new file mode 100644 index 0000000..b5e164b --- /dev/null +++ b/src/YesNt.Interpreter/Statements/MapStatements.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; + +using YesNt.Interpreter.Attributes; +using YesNt.Interpreter.Enums; +using YesNt.Interpreter.Runtime; + +namespace YesNt.Interpreter.Statements; + +internal class MapStatements : StatementRuntimeInformation +{ + [Statement("map", 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.Maps.TryGetValue(name, out Dictionary value)) + { + RuntimeInfo.Maps.Add(name, []); + } + else + { + value.Clear(); + } + } + + [Statement("map", 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.Maps.ContainsKey(name)) + { + RuntimeInfo.Exit(ExitMessages.MapNotFound(name), true); + return; + } + + _ = RuntimeInfo.Maps.Remove(name); + } + + [Statement("map", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " clear")] + public void Clear(string args) + { + string[] parts = SplitTwo(args, " clear"); + if (parts.Length == 0) + { + return; + } + + if (!TryGetMap(parts[0], out Dictionary map)) + { + return; + } + + map.Clear(); + } + + [Statement("map", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " size")] + public void Size(string args) + { + string[] parts = SplitTwo(args, " size"); + if (parts.Length == 0) + { + return; + } + + if (!TryGetMap(parts[0], out Dictionary map)) + { + return; + } + + RuntimeInfo.OutParametersStack.Clear(); + RuntimeInfo.OutParametersStack.Push(map.Count.ToString()); + } + + [Statement("map", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " put ")] + public void Put(string args) + { + string[] parts = SplitTwo(args, " put "); + if (parts.Length == 0) + { + return; + } + + if (!TryGetMap(parts[0], out Dictionary map)) + { + return; + } + + string[] keyAndValue = SplitKeyAndValue(parts[1]); + if (keyAndValue.Length == 0) + { + return; + } + + map[keyAndValue[0]] = keyAndValue[1]; + } + + [Statement("map", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " get ")] + public void Get(string args) + { + string[] parts = SplitTwo(args, " get "); + if (parts.Length == 0) + { + return; + } + + if (!TryGetMap(parts[0], out Dictionary map)) + { + return; + } + + string key = parts[1]; + if (!map.TryGetValue(key, out string value)) + { + RuntimeInfo.Exit(ExitMessages.KeyNotFound(key), true); + return; + } + + RuntimeInfo.OutParametersStack.Clear(); + RuntimeInfo.OutParametersStack.Push(value); + } + + [Statement("map", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " has ")] + public void Has(string args) + { + string[] parts = SplitTwo(args, " has "); + if (parts.Length == 0) + { + return; + } + + if (!TryGetMap(parts[0], out Dictionary map)) + { + return; + } + + RuntimeInfo.OutParametersStack.Clear(); + RuntimeInfo.OutParametersStack.Push(map.ContainsKey(parts[1]).ToString()); + } + + [Statement("map", SearchMode.StartOfLine, SpaceAround.End, ConsoleColor.DarkCyan, Separator = " remove ")] + public void Remove(string args) + { + string[] parts = SplitTwo(args, " remove "); + if (parts.Length == 0) + { + return; + } + + if (!TryGetMap(parts[0], out Dictionary map)) + { + return; + } + + _ = map.Remove(parts[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 TryGetMap(string name, out Dictionary map) + { + if (!RuntimeInfo.Maps.TryGetValue(name, out map)) + { + RuntimeInfo.Exit(ExitMessages.MapNotFound(name), true); + return false; + } + + return true; + } + + private string[] SplitKeyAndValue(string input) + { + string[] parts = input.Split(','); + 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]) || string.IsNullOrWhiteSpace(parts[1])) + { + RuntimeInfo.Exit(ExitMessages.InvalidSyntax, true); + return []; + } + + return parts; + } +}