Add support for maps

This commit is contained in:
Stone_Red
2026-08-07 23:36:02 +02:00
parent 4626b5a7fa
commit 7d645f3c6e
6 changed files with 613 additions and 4 deletions
+106 -4
View File
@@ -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 keyvalue 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 <name> new
```
Creates an empty map. If the map already exists it is cleared.
### Add or update an entry - `map … put`
```
map <name> put <key>, <value>
```
Sets `<key>` to `<value>`. 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 <name> get <key>
```
Pushes the value stored under `<key>` onto the output stack. Access it with `%out`. Terminates with an error if the key does not exist.
### Check a key - `map … has`
```
map <name> has <key>
```
Pushes `True` if `<key>` 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 <name> remove <key>
```
Removes `<key>` and its value. Removing a key that does not exist is silently ignored.
### Get the size - `map … size`
```
map <name> 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 <name> clear
```
Removes all entries but keeps the map alive.
### Delete a map - `map … delete`
```
map <name> 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 |
@@ -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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<string> 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<string> lines =
[
"map missing size"
];
YesNtAssert.ContainsTerminationMessage(lines, "Map \"missing\" not found");
}
[TestMethod]
public void MapGetMissingKeyFailsTest()
{
List<string> lines =
[
"map settings new",
"map settings get a"
];
YesNtAssert.ContainsTerminationMessage(lines, "Key \"a\" not found");
}
[TestMethod]
public void MapInvalidSyntaxFailsTest()
{
List<string> lines =
[
"map settings put"
];
YesNtAssert.ContainsTerminationMessage(lines, "Invalid statement");
}
[TestMethod]
public void MapPutExtraCommaFailsTest()
{
List<string> lines =
[
"map settings new",
"map settings put test, abc, htuerthieu"
];
YesNtAssert.ContainsTerminationMessage(lines, "Invalid syntax");
}
[TestMethod]
public void MapScopeInsideFunctionTest()
{
List<string> 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<string> 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<string> 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<string> 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<string> lines =
[
"map settings new",
"map settings put name, \"Doe, John\"",
"map settings get name",
"var result = %out eval",
"${result}"
];
YesNtAssert.IsLastLineEqual(lines, "Doe, John");
}
}
@@ -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";
@@ -17,6 +17,9 @@ internal class FunctionScope(int callerLine, Stack<string> arguments)
/// <summary>Gets the local list table for this function invocation.</summary>
public Dictionary<string, List<string>> Lists { get; } = [];
/// <summary>Gets the local map table for this function invocation.</summary>
public Dictionary<string, Dictionary<string, string>> Maps { get; } = [];
/// <summary>Gets the local label table for this function invocation.</summary>
public Dictionary<string, int> Labels { get; } = [];
@@ -24,6 +24,7 @@ internal sealed class RuntimeInformation : IStatementContext
private static int internalTaskId = 0;
private readonly Dictionary<string, string> topVariables = [];
private readonly Dictionary<string, List<string>> topLists = [];
private readonly Dictionary<string, Dictionary<string, string>> topMaps = [];
public Dictionary<string, string> GlobalVariables { get; set; } = [];
public Dictionary<string, int> Functions { get; } = [];
@@ -54,6 +55,7 @@ internal sealed class RuntimeInformation : IStatementContext
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, Dictionary<string, string>> Maps => FunctionCallStack.Count == 0 ? topMaps : FunctionCallStack.Peek().Maps;
public Dictionary<string, int> 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();
@@ -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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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<string, string> 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;
}
}