diff --git a/README.md b/README.md index bb6b500..d864e17 100644 --- a/README.md +++ b/README.md @@ -15,37 +15,40 @@ Below is an example that fills a destination span with coordinates for the point with either a radius or a diameter as input. While reusing the same machine instance by modifying its source and variables, and re-evaluating the expression. ```cs -public unsafe void GetCirclePositions(float value, bool isDiameter, USpan positions) +public void GetCirclePositions(float radius, Span positions) { using Machine vm = new(); vm.SetVariable("value", value); - vm.SetVariable("multiplier", isDiameter ? 2 : 1); - vm.SetFunction("cos", &Cos); - vm.SetFunction("sin", &Sin); + vm.SetFunction("cos", MathF.Cos); + vm.SetFunction("sin", MathF.Sin); - uint length = positions.Length; + int length = positions.Length; for (int i = 0; i < length; i++) { float t = i * MathF.PI / (length * 0.5f); vm.SetVariable("t", t); - vm.SetSource("cos(t) * (value * multiplier)"); + vm.SetSource("cos(t) * radius"); float x = vm.Evaluate(); - vm.SetSource("sin(t) * (value * multiplier)"); + vm.SetSource("sin(t) * radius"); float y = vm.Evaluate(); positions[i] = new Vector2(x, y); } +} +``` - [UnmanagedCallersOnly] - static float Cos(float value) - { - return MathF.Cos(value); - } +### Checking for compilation issues - [UnmanagedCallersOnly] - static float Sin(float value) - { - return MathF.Sin(value); - } +When a text source is assigned to the machine, it returns a compilation result. +This result value can be used to check if there were issues. And can do so with the try-do pattern: +```cs +if (vm.TrySetSource("5 +", out Exception? exception)) +{ + //success +} +else +{ + //error + throw exception; } ``` diff --git a/source/CompilationResult.cs b/source/CompilationResult.cs new file mode 100644 index 0000000..78ae573 --- /dev/null +++ b/source/CompilationResult.cs @@ -0,0 +1,60 @@ +using System; + +namespace ExpressionMachine +{ + /// + /// Represents the result of compilation. + /// + public readonly struct CompilationResult : IEquatable + { + /// + /// Success compilation result. + /// + public static CompilationResult Success => new(null); + + /// + /// Compilation exception if there was one. + /// + public readonly Exception? exception; + + /// + /// Checks if the compilation was successful. + /// + public readonly bool IsSuccess => exception is null; + + internal CompilationResult(Exception? exception) + { + this.exception = exception; + } + + /// + public readonly override bool Equals(object? obj) + { + return obj is CompilationResult result && Equals(result); + } + + /// + public readonly bool Equals(CompilationResult other) + { + return exception == other.exception; + } + + /// + public readonly override int GetHashCode() + { + return exception?.GetHashCode() ?? 0; + } + + /// + public static bool operator ==(CompilationResult left, CompilationResult right) + { + return left.Equals(right); + } + + /// + public static bool operator !=(CompilationResult left, CompilationResult right) + { + return !(left == right); + } + } +} \ No newline at end of file diff --git a/source/Exceptions/MissingTokenException.cs b/source/Exceptions/MissingTokenException.cs new file mode 100644 index 0000000..0e8f567 --- /dev/null +++ b/source/Exceptions/MissingTokenException.cs @@ -0,0 +1,20 @@ +using System; + +namespace ExpressionMachine +{ + /// + /// Error when an expected token is missing. + /// + public class MissingTokenException : Exception + { + + } + + /// + /// Error when a token to close a group is missing. + /// + public class MissingGroupCloseToken : Exception + { + + } +} diff --git a/source/ExpressionMachine.csproj b/source/ExpressionMachine.csproj index e6c2fde..f0c1eee 100644 --- a/source/ExpressionMachine.csproj +++ b/source/ExpressionMachine.csproj @@ -6,6 +6,12 @@ enable True True + Expression Machine + popcron + simulation-tree + Library for evaluating logic expressions at runtime. + README.md + https://github.com/simulation-tree/expression-machine @@ -23,6 +29,13 @@ 7 + + + True + \ + + + diff --git a/source/Machine.cs b/source/Machine.cs index fe40b8d..87618e4 100644 --- a/source/Machine.cs +++ b/source/Machine.cs @@ -1,7 +1,7 @@ -using Collections; -using Collections.Generic; +using Collections.Generic; using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Unmanaged; @@ -24,7 +24,7 @@ public readonly ReadOnlySpan Source { MemoryAddress.ThrowIfDefault(machine); - return machine->source.AsSpan(); + return machine->source.GetSpan(machine->sourceLength); } } @@ -39,25 +39,18 @@ public readonly ReadOnlySpan Source /// public Machine() { - machine = Implementation.Allocate(); + machine = MemoryAddress.AllocatePointer(); + machine[0] = new(TokenMap.Create()); } #endif - /// - /// Initializes an existing machine from the given . - /// - public Machine(Implementation* pointer) - { - this.machine = pointer; - } - /// /// Creates a new machine initialized with the given . /// public Machine(ReadOnlySpan source) { - machine = Implementation.Allocate(); - SetSource(source); + machine = MemoryAddress.AllocatePointer(); + machine[0] = new(TokenMap.Create(), source); } /// @@ -65,8 +58,10 @@ public Machine(ReadOnlySpan source) /// public Machine(ASCIIText256 source) { - machine = Implementation.Allocate(); - SetSource(source); + machine = MemoryAddress.AllocatePointer(); + Span buffer = stackalloc char[source.Length]; + source.CopyTo(buffer); + machine[0] = new(TokenMap.Create(), buffer); } /// @@ -74,8 +69,8 @@ public Machine(ASCIIText256 source) /// public Machine(string source) { - machine = Implementation.Allocate(); - SetSource(source); + machine = MemoryAddress.AllocatePointer(); + machine[0] = new(TokenMap.Create(), source); } [Conditional("DEBUG")] @@ -105,49 +100,148 @@ private readonly void ThrowIfFunctionIsMissing(ASCIIText256 name) } } + [Conditional("DEBUG")] + private readonly void ThrowIfTokenIsOutOfRange(int start, int length) + { + if (start < 0) + { + throw new InvalidOperationException($"Start index `{start}` is out of bounds"); + } + + if (length < 0) + { + throw new InvalidOperationException($"Length `{length}` is out of bounds"); + } + + if (start + length > machine->sourceLength) + { + throw new InvalidOperationException($"Token starting at `{start}` with length `{length}` is out of bounds"); + } + } + /// public void Dispose() { MemoryAddress.ThrowIfDefault(machine); - Implementation.Free(ref machine); + foreach (Function function in machine->functionValues.Values) + { + function.Dispose(); + } + + machine->tree.Dispose(); + machine->tokens.Dispose(); + machine->functionValues.Dispose(); + machine->variableValues.Dispose(); + machine->source.Dispose(); + machine->map.Dispose(); + MemoryAddress.Free(ref machine); } /// /// Assigns to the machine. /// - public readonly void SetSource(ReadOnlySpan newSource) + public readonly bool TrySetSource(ReadOnlySpan newSource, [NotNullWhen(false)] out Exception? exception) { MemoryAddress.ThrowIfDefault(machine); - ReadOnlySpan currentSource = machine->source.AsSpan(); + ReadOnlySpan currentSource = machine->source.GetSpan(machine->sourceLength); if (!newSource.SequenceEqual(currentSource)) { + machine->sourceLength = newSource.Length; + if (machine->sourceCapacity < machine->sourceLength) + { + machine->sourceCapacity = machine->sourceLength.GetNextPowerOf2(); + MemoryAddress.Resize(ref machine->source, machine->sourceCapacity * sizeof(char)); + } + + machine->source.CopyFrom(newSource); + machine->tokens.Clear(); + Parsing.GetTokens(newSource, machine->map, machine->tokens); + + //todo: efficiency: instead of disposing and creating a new instance, reuse it + machine->tree.Dispose(); + if (Parsing.TryGetTree(machine->tokens.AsSpan(), out machine->tree, out exception)) + { + return true; + } + else + { + machine->tree = Node.Create(); + return false; + } + } + + exception = default; + return true; + } + + /// + /// Assigns to the machine. + /// + public readonly CompilationResult SetSource(ReadOnlySpan newSource) + { + MemoryAddress.ThrowIfDefault(machine); + + ReadOnlySpan currentSource = machine->source.GetSpan(machine->sourceLength); + if (!newSource.SequenceEqual(currentSource)) + { + machine->sourceLength = newSource.Length; + if (machine->sourceCapacity < machine->sourceLength) + { + machine->sourceCapacity = machine->sourceLength.GetNextPowerOf2(); + MemoryAddress.Resize(ref machine->source, machine->sourceCapacity * sizeof(char)); + } + machine->source.CopyFrom(newSource); machine->tokens.Clear(); Parsing.GetTokens(newSource, machine->map, machine->tokens); machine->tree.Dispose(); - machine->tree = Parsing.GetTree(machine->tokens.AsSpan()); + if (!Parsing.TryGetTree(machine->tokens.AsSpan(), out machine->tree, out Exception? exception)) + { + machine->tree = Node.Create(); + return new(exception); + } } + + return CompilationResult.Success; } /// /// Assigns to the machine. /// - public readonly void SetSource(ASCIIText256 newSource) + public readonly bool TrySetSource(ASCIIText256 newSource, [NotNullWhen(false)] out Exception? exception) { Span nameSpan = stackalloc char[newSource.Length]; newSource.CopyTo(nameSpan); - SetSource(nameSpan); + return TrySetSource(nameSpan, out exception); } /// /// Assigns to the machine. /// - public readonly void SetSource(string newSource) + public readonly bool TrySetSource(string newSource, [NotNullWhen(false)] out Exception? exception) { - SetSource(newSource.AsSpan()); + return TrySetSource(newSource.AsSpan(), out exception); + } + + /// + /// Assigns to the machine. + /// + public readonly CompilationResult SetSource(ASCIIText256 newSource) + { + Span nameSpan = stackalloc char[newSource.Length]; + newSource.CopyTo(nameSpan); + return SetSource(nameSpan); + } + + /// + /// Assigns to the machine. + /// + public readonly CompilationResult SetSource(string newSource) + { + return SetSource(newSource.AsSpan()); } /// @@ -157,7 +251,6 @@ public readonly void ClearVariables() { MemoryAddress.ThrowIfDefault(machine); - machine->variableNameHashes.Clear(); machine->variableValues.Clear(); } @@ -168,7 +261,6 @@ public readonly void ClearFunctions() { MemoryAddress.ThrowIfDefault(machine); - machine->functionNameHashes.Clear(); machine->functionValues.Clear(); } @@ -180,9 +272,8 @@ public readonly float GetVariable(ReadOnlySpan name) MemoryAddress.ThrowIfDefault(machine); ThrowIfVariableIsMissing(name); - int hash = new ASCIIText256(name).GetHashCode(); - int index = machine->variableNameHashes.IndexOf(hash); - return machine->variableValues[index]; + long hash = name.GetLongHashCode(); + return machine->variableValues[hash]; } /// @@ -210,8 +301,8 @@ public readonly bool ContainsVariable(ReadOnlySpan name) { MemoryAddress.ThrowIfDefault(machine); - int hash = new ASCIIText256(name).GetHashCode(); - return machine->variableNameHashes.Contains(hash); + long hash = name.GetLongHashCode(); + return machine->variableValues.ContainsKey(hash); } /// @@ -239,8 +330,8 @@ public readonly bool ContainsFunction(ReadOnlySpan name) { MemoryAddress.ThrowIfDefault(machine); - int hash = new ASCIIText256(name).GetHashCode(); - return machine->functionNameHashes.Contains(hash); + long hash = name.GetLongHashCode(); + return machine->functionValues.ContainsKey(hash); } /// @@ -250,8 +341,8 @@ public readonly bool ContainsFunction(ASCIIText256 name) { MemoryAddress.ThrowIfDefault(machine); - int hash = name.GetHashCode(); - return machine->functionNameHashes.Contains(hash); + long hash = name.GetLongHashCode(); + return machine->functionValues.ContainsKey(hash); } /// @@ -261,16 +352,14 @@ public readonly void SetVariable(ReadOnlySpan name, float value) { MemoryAddress.ThrowIfDefault(machine); - int hash = new ASCIIText256(name).GetHashCode(); - if (machine->variableNameHashes.TryIndexOf(hash, out int index)) + long hash = name.GetLongHashCode(); + ref float existing = ref machine->variableValues.TryGetValue(hash, out bool contains); + if (!contains) { - machine->variableValues[index] = value; - } - else - { - machine->variableNameHashes.Add(hash); - machine->variableValues.Add(value); + existing = ref machine->variableValues.Add(hash); } + + existing = value; } /// @@ -297,8 +386,9 @@ public readonly void SetVariable(string name, float value) public readonly ReadOnlySpan GetToken(Token token) { MemoryAddress.ThrowIfDefault(machine); + ThrowIfTokenIsOutOfRange(token.start, token.length); - return machine->source.AsSpan().Slice(token.start, token.length); + return machine->source.AsSpan(token.start, token.length); } /// @@ -307,8 +397,9 @@ public readonly ReadOnlySpan GetToken(Token token) public readonly ReadOnlySpan GetToken(int start, int length) { MemoryAddress.ThrowIfDefault(machine); + ThrowIfTokenIsOutOfRange(start, length); - return machine->source.AsSpan().Slice(start, length); + return machine->source.AsSpan(start, length); } /// @@ -318,16 +409,14 @@ public readonly void SetFunction(ASCIIText256 name, Function function) { MemoryAddress.ThrowIfDefault(machine); - int hash = name.GetHashCode(); - if (machine->functionNameHashes.TryIndexOf(hash, out int index)) + long hash = name.GetLongHashCode(); + ref Function existing = ref machine->functionValues.TryGetValue(hash, out bool contains); + if (!contains) { - machine->functionValues[index] = function; - } - else - { - machine->functionNameHashes.Add(hash); - machine->functionValues.Add(function); + existing = ref machine->functionValues.Add(hash); } + + existing = function; } /// @@ -337,17 +426,14 @@ public readonly void SetFunction(ReadOnlySpan name, delegate* unmanagedfunctionNameHashes.TryIndexOf(hash, out int index)) + long hash = name.GetLongHashCode(); + ref Function existing = ref machine->functionValues.TryGetValue(hash, out bool contains); + if (!contains) { - machine->functionValues[index] = f; - } - else - { - machine->functionNameHashes.Add(hash); - machine->functionValues.Add(f); + existing = ref machine->functionValues.Add(hash); } + + existing = new(function); } /// @@ -375,17 +461,14 @@ public readonly void SetFunction(ReadOnlySpan name, Func fun { MemoryAddress.ThrowIfDefault(machine); - Function f = new(function); - int hash = new ASCIIText256(name).GetHashCode(); - if (machine->functionNameHashes.TryIndexOf(hash, out int index)) - { - machine->functionValues[index] = f; - } - else + long hash = name.GetLongHashCode(); + ref Function existing = ref machine->functionValues.TryGetValue(hash, out bool contains); + if (!contains) { - machine->functionNameHashes.Add(hash); - machine->functionValues.Add(f); + existing = ref machine->functionValues.Add(hash); } + + existing = new(function); } /// @@ -415,9 +498,8 @@ public readonly float InvokeFunction(ReadOnlySpan name, float value) MemoryAddress.ThrowIfDefault(machine); ThrowIfFunctionIsMissing(name); - int hash = new ASCIIText256(name).GetHashCode(); - int index = machine->functionNameHashes.IndexOf(hash); - Function function = machine->functionValues[index]; + long hash = name.GetLongHashCode(); + Function function = machine->functionValues[hash]; return function.Invoke(value); } @@ -430,9 +512,8 @@ public readonly float InvokeFunction(ASCIIText256 name, float value) MemoryAddress.ThrowIfDefault(machine); ThrowIfFunctionIsMissing(name); - int hash = name.GetHashCode(); - int index = machine->functionNameHashes.IndexOf(hash); - Function function = machine->functionValues[index]; + long hash = name.GetLongHashCode(); + Function function = machine->functionValues[hash]; return function.Invoke(value); } @@ -445,9 +526,8 @@ public readonly float InvokeFunction(string name, float value) MemoryAddress.ThrowIfDefault(machine); ThrowIfFunctionIsMissing(name); - int hash = new ASCIIText256(name).GetHashCode(); - int index = machine->functionNameHashes.IndexOf(hash); - Function function = machine->functionValues[index]; + long hash = name.GetLongHashCode(); + Function function = machine->functionValues[hash]; return function.Invoke(value); } @@ -458,7 +538,7 @@ public readonly float Evaluate() { MemoryAddress.ThrowIfDefault(machine); - ReadOnlySpan source = machine->source.AsSpan(); + ReadOnlySpan source = machine->source.GetSpan(machine->sourceLength); if (float.TryParse(source, out float result)) { return result; @@ -472,63 +552,44 @@ public readonly float Evaluate() /// /// Native implementation type. /// - public struct Implementation + internal struct Implementation { - internal readonly TokenMap map; - internal readonly Text source; - internal readonly List variableNameHashes; - internal readonly List variableValues; - internal readonly List functionNameHashes; - internal readonly List functionValues; - internal readonly List tokens; - internal Node tree; + public Node tree; + public MemoryAddress source; + public int sourceLength; + public int sourceCapacity; + public readonly TokenMap map; + public readonly Dictionary variableValues; + public readonly Dictionary functionValues; + public readonly List tokens; - private Implementation(TokenMap map) + public Implementation(TokenMap map) { this.map = map; - source = new(); - variableNameHashes = new(); - variableValues = new(); - functionNameHashes = new(); - functionValues = new(); - tokens = new(); - tree = new(); - } - - /// - /// Allocates a new machine. - /// - public static Implementation* Allocate() - { - ref Implementation machine = ref MemoryAddress.Allocate(); - machine = new(new TokenMap()); - fixed (Implementation* pointer = &machine) - { - return pointer; - } + sourceCapacity = 4; + sourceLength = 0; + source = MemoryAddress.Allocate(sizeof(char) * sourceCapacity); + variableValues = new(4); + functionValues = new(4); + tokens = new(32); + tree = Node.Create(); } - /// - /// Frees the given . - /// - public static void Free(ref Implementation* machine) + public Implementation(TokenMap map, ReadOnlySpan source) { - MemoryAddress.ThrowIfDefault(machine); - - for (int i = 0; i < machine->functionValues.Count; i++) + this.map = map; + sourceLength = source.Length; + sourceCapacity = Math.Max(4, sourceLength.GetNextPowerOf2()); + this.source = MemoryAddress.Allocate(sizeof(char) * sourceCapacity); + this.source.CopyFrom(source); + variableValues = new(4); + functionValues = new(4); + tokens = Parsing.GetTokens(source, map); + if (!Parsing.TryGetTree(tokens.AsSpan(), out tree, out Exception? exception)) { - machine->functionValues[i].Dispose(); + tree = Node.Create(); + throw exception; } - - machine->tree.Dispose(); - machine->tokens.Dispose(); - machine->functionValues.Dispose(); - machine->functionNameHashes.Dispose(); - machine->variableValues.Dispose(); - machine->variableNameHashes.Dispose(); - machine->source.Dispose(); - machine->map.Dispose(); - MemoryAddress.Free(ref machine); } } } diff --git a/source/Node.cs b/source/Node.cs index ac9d4cd..01ca0ef 100644 --- a/source/Node.cs +++ b/source/Node.cs @@ -6,7 +6,7 @@ namespace ExpressionMachine /// /// Represents a node in the expression tree. /// - public unsafe struct Node : IDisposable + public unsafe struct Node : IDisposable, IEquatable { private Implementation* node; @@ -78,24 +78,24 @@ public readonly ref nint C /// public Node() { - node = Implementation.Allocate(default, default, default, default); + node = MemoryAddress.AllocatePointer(); + node[0] = new(default, default, default, default); } #endif /// - /// Initializes an existing node from the given . + /// Creates a new node with the given . /// - public Node(Implementation* pointer) + public Node(NodeType type, nint a, nint b, nint c) { - this.node = pointer; + node = MemoryAddress.AllocatePointer(); + node[0] = new(type, a, b, c); } - /// - /// Creates a new node with the given . - /// - public Node(NodeType type, nint a, nint b, nint c) + /// + public readonly override string ToString() { - node = Implementation.Allocate(type, a, b, c); + return Type.ToString(); } /// @@ -127,17 +127,58 @@ public readonly void Clear() node->c = default; } + /// + public readonly override bool Equals(object? obj) + { + return obj is Node node && Equals(node); + } + + /// + public readonly bool Equals(Node other) + { + return node == other.node; + } + + /// + public readonly override int GetHashCode() + { + unchecked + { + return ((nint)node).GetHashCode(); + } + } + + /// + /// Creates an empty node. + /// + public static Node Create() + { + return new(default, default, default, default); + } + + /// + public static bool operator ==(Node left, Node right) + { + return left.Equals(right); + } + + /// + public static bool operator !=(Node left, Node right) + { + return !(left == right); + } + /// /// Implementation type. /// - public struct Implementation + internal struct Implementation { internal NodeType type; internal nint a; internal nint b; internal nint c; - private Implementation(NodeType type, nint a, nint b, nint c) + public Implementation(NodeType type, nint a, nint b, nint c) { this.type = type; this.a = a; @@ -217,19 +258,6 @@ public static float Evaluate(Implementation* node, Machine vm) throw new InvalidOperationException($"Unknown node type `{type}`"); } } - - /// - /// Allocates a new node. - /// - public static Implementation* Allocate(NodeType type, nint a, nint b, nint c) - { - ref Implementation node = ref MemoryAddress.Allocate(); - node = new Implementation(type, a, b, c); - fixed (Implementation* pointer = &node) - { - return pointer; - } - } } } } \ No newline at end of file diff --git a/source/Parsing.cs b/source/Parsing.cs index 6cbfce9..51b1748 100644 --- a/source/Parsing.cs +++ b/source/Parsing.cs @@ -1,6 +1,6 @@ -using Collections; -using Collections.Generic; +using Collections.Generic; using System; +using System.Diagnostics.CodeAnalysis; namespace ExpressionMachine { @@ -45,16 +45,18 @@ public static void GetTokens(ReadOnlySpan expression, TokenMap map, List ignore = map.Ignore; + ReadOnlySpan tokens = map.Tokens; while (position < length) { char current = expression[position]; - if (map.Ignore.Contains(current)) + if (ignore.Contains(current)) { position++; continue; } - if (map.Tokens.TryIndexOf(current, out int tokenIndex)) + if (tokens.TryIndexOf(current, out int tokenIndex)) { Token.Type type = (Token.Type)tokenIndex; list.Add(new(type, position, 1)); @@ -67,7 +69,7 @@ public static void GetTokens(ReadOnlySpan expression, TokenMap map, List expression, TokenMap map, List containing the expression represented /// by the given . /// - public static Node GetTree(ReadOnlySpan tokens) + public static bool TryGetTree(ReadOnlySpan tokens, out Node node, [NotNullWhen(false)] out Exception? exception) { int position = 0; - return new(TryParseExpression(ref position, tokens)); + return TryParseExpression(ref position, tokens, out node, out exception); } - private static Node.Implementation* TryParseExpression(ref int position, ReadOnlySpan tokens) + private static bool TryParseExpression(ref int position, ReadOnlySpan tokens, out Node node, [NotNullWhen(false)] out Exception? exception) { //todo: handle control nodes like if, else if, else, do, goto, and while - Node.Implementation* result = TryReadFactor(ref position, tokens); - if (position == tokens.Length) - { - return result; - } - - Token current = tokens[position]; - while (result is not null && position < tokens.Length && IsTerm(current.type)) + if (TryReadFactor(ref position, tokens, out node, out exception)) { - if (current.type == Token.Type.Add) - { - position++; - Node.Implementation* right = TryReadFactor(ref position, tokens); - result = Node.Implementation.Allocate(NodeType.Addition, (nint)result, (nint)right, default); - } - else if (current.type == Token.Type.Subtract) + if (position == tokens.Length) { - position++; - Node.Implementation* right = TryReadFactor(ref position, tokens); - result = Node.Implementation.Allocate(NodeType.Subtraction, (nint)result, (nint)right, default); + return true; } - if (position == tokens.Length) + Token current = tokens[position]; + while (node != default && position < tokens.Length && IsTerm(current.type)) { - break; - } + if (current.type == Token.Type.Add) + { + position++; + if (TryReadFactor(ref position, tokens, out Node right, out exception)) + { + node = new(NodeType.Addition, node.Address, right.Address, default); + } + else + { + node.Dispose(); + node = default; + return false; + } + } + else if (current.type == Token.Type.Subtract) + { + position++; + if (TryReadFactor(ref position, tokens, out Node right, out exception)) + { + node = new(NodeType.Subtraction, node.Address, right.Address, default); + } + else + { + node.Dispose(); + node = default; + return false; + } + } - current = tokens[position]; - } + if (position == tokens.Length) + { + break; + } - return result; - } + current = tokens[position]; + } - private static Node.Implementation* TryReadFactor(ref int position, ReadOnlySpan tokens) - { - Node.Implementation* factor = TryReadTerm(ref position, tokens); - if (position == tokens.Length) + return true; + } + else { - return factor; + node = default; + return false; } + } - Token current = tokens[position]; - while (factor is not null && position < tokens.Length && IsFactor(current.type)) + private static bool TryReadFactor(ref int position, ReadOnlySpan tokens, out Node node, [NotNullWhen(false)] out Exception? exception) + { + if (TryReadTerm(ref position, tokens, out node, out exception)) { - if (current.type == Token.Type.Multiply) - { - position++; - Node.Implementation* right = TryReadTerm(ref position, tokens); - factor = Node.Implementation.Allocate(NodeType.Multiplication, (nint)factor, (nint)right, default); - } - else if (current.type == Token.Type.Divide) + if (position == tokens.Length) { - position++; - Node.Implementation* right = TryReadTerm(ref position, tokens); - factor = Node.Implementation.Allocate(NodeType.Division, (nint)factor, (nint)right, default); + return true; } - if (position == tokens.Length) + Token current = tokens[position]; + while (node != default && position < tokens.Length && IsFactor(current.type)) { - break; + if (current.type == Token.Type.Multiply) + { + position++; + if (TryReadTerm(ref position, tokens, out Node right, out exception)) + { + node = new(NodeType.Multiplication, node.Address, right.Address, default); + } + else + { + node.Dispose(); + node = default; + return false; + } + } + else if (current.type == Token.Type.Divide) + { + position++; + if (TryReadTerm(ref position, tokens, out Node right, out exception)) + { + node = new(NodeType.Division, node.Address, right.Address, default); + } + else + { + node.Dispose(); + node = default; + return false; + } + } + + if (position == tokens.Length) + { + break; + } + + current = tokens[position]; } - current = tokens[position]; + return true; + } + else + { + node = default; + return false; } - - return factor; } - private static Node.Implementation* TryReadTerm(ref int position, ReadOnlySpan tokens) + private static bool TryReadTerm(ref int position, ReadOnlySpan tokens, out Node node, [NotNullWhen(false)] out Exception? exception) { if (position == tokens.Length) { Token lastToken = tokens[position - 1]; - throw new FormatException($"Expected a token after {lastToken}"); + node = default; + exception = new MissingTokenException(); + return false; } Token current = tokens[position]; position++; if (current.type == Token.Type.BeginGroup) { - Node.Implementation* term = TryParseExpression(ref position, tokens); - current = tokens[position]; - if (current.type != Token.Type.EndGroup) + if (TryParseExpression(ref position, tokens, out node, out exception)) { - throw new FormatException("Expected closing parenthesis"); - } + current = tokens[position]; + if (current.type != Token.Type.EndGroup) + { + node.Dispose(); + node = default; + exception = new MissingGroupCloseToken(); + return false; + } - position++; - return term; + position++; + return true; + } + else + { + node = default; + return false; + } } else if (current.type == Token.Type.Value) { @@ -195,27 +255,43 @@ public static Node GetTree(ReadOnlySpan tokens) if (next.type == Token.Type.BeginGroup) { position++; - Node.Implementation* argument = TryParseExpression(ref position, tokens); - if (position < tokens.Length) + if (TryParseExpression(ref position, tokens, out Node argument, out exception)) { - current = tokens[position]; - if (current.type != Token.Type.EndGroup) + if (position < tokens.Length) { - throw new FormatException("Expected closing parenthesis"); + current = tokens[position]; + if (current.type != Token.Type.EndGroup) + { + argument.Dispose(); + node = default; + exception = new MissingGroupCloseToken(); + return false; + } + + position++; } - position++; + node = new(NodeType.Call, start, length, argument.Address); + exception = null; + return true; + } + else + { + node = default; + return false; } - - return Node.Implementation.Allocate(NodeType.Call, start, length, (nint)argument); } } - return Node.Implementation.Allocate(NodeType.Value, start, length, default); + node = new(NodeType.Value, start, length, default); + exception = null; + return true; } else { - return null; + node = default; + exception = null; + return true; } } diff --git a/source/TokenMap.cs b/source/TokenMap.cs index 7bb8634..a92e498 100644 --- a/source/TokenMap.cs +++ b/source/TokenMap.cs @@ -52,7 +52,7 @@ public readonly ReadOnlySpan Ignore public TokenMap() { Token.Type[] options = Enum.GetValues(); - tokens = new((int)options.Length); + tokens = new(options.Length); tokens[(int)Token.Type.Value] = default; tokens[(int)Token.Type.Add] = '+'; tokens[(int)Token.Type.Subtract] = '-'; @@ -66,6 +66,12 @@ public TokenMap() ignore.Append('\t'); } + private TokenMap(Text tokens, Text ignore) + { + this.tokens = tokens; + this.ignore = ignore; + } + /// /// Disposes the token map. /// @@ -85,5 +91,26 @@ private readonly void ThrowIfDisposed() throw new ObjectDisposedException(nameof(TokenMap)); } } + + /// + /// Creates a default token map. + /// + public static TokenMap Create() + { + Token.Type[] options = Enum.GetValues(); + Text tokens = new(options.Length); + tokens[(int)Token.Type.Value] = default; + tokens[(int)Token.Type.Add] = '+'; + tokens[(int)Token.Type.Subtract] = '-'; + tokens[(int)Token.Type.Multiply] = '*'; + tokens[(int)Token.Type.Divide] = '/'; + tokens[(int)Token.Type.BeginGroup] = '('; + tokens[(int)Token.Type.EndGroup] = ')'; + + Text ignore = new(0); + ignore.Append(' '); + ignore.Append('\t'); + return new(tokens, ignore); + } } } \ No newline at end of file diff --git a/tests/ExpressionMachineTests.cs b/tests/ExpressionMachineTests.cs index d997389..f8a0450 100644 --- a/tests/ExpressionMachineTests.cs +++ b/tests/ExpressionMachineTests.cs @@ -1,5 +1,4 @@ -using Collections.Generic; -using System; +using System; using System.Numerics; using Unmanaged.Tests; @@ -75,6 +74,8 @@ public void AnchorExample() vertical.SetVariable("height", 600); vertical.SetFunction("multiply", Multiply); + Assert.That(horizontal.Source.ToString(), Is.EqualTo("width * 0.5")); + Assert.That(vertical.Source.ToString(), Is.EqualTo("multiply(height) + 50")); Assert.That(horizontal.Evaluate(), Is.EqualTo(400)); Assert.That(vertical.Evaluate(), Is.EqualTo(350)); @@ -112,25 +113,25 @@ public void PrintCircle() vm.SetFunction("cos", MathF.Cos); vm.SetFunction("sin", MathF.Sin); - using List positions = new(); - for (uint i = 0; i < 360; i++) + Span positions = stackalloc Vector2[360]; + int length = positions.Length; + for (int i = 0; i < positions.Length; i++) { - float t = i * MathF.PI / 180; + float t = i * MathF.PI / (length * 0.5f); vm.SetVariable("t", t); vm.SetSource("cos(t) * radius"); float x = vm.Evaluate(); vm.SetSource("sin(t) * radius"); float y = vm.Evaluate(); - positions.Add(new Vector2(x, y)); + positions[i] = new Vector2(x, y); } - using List otherPositions = new(); - for (uint i = 0; i < 360; i++) + Span otherPositions = stackalloc Vector2[positions.Length]; + for (int i = 0; i < positions.Length; i++) { - float t = i * MathF.PI / 180; - float x = MathF.Cos(t) * radius; - float y = MathF.Sin(t) * radius; - otherPositions.Add(new Vector2(x, y)); + float t = i * MathF.PI / (length * 0.5f); + (float y, float x) = MathF.SinCos(t); + otherPositions[i] = new Vector2(x, y) * radius; } for (int i = 0; i < 360; i++) @@ -150,5 +151,23 @@ public void UseNodes() a.Dispose(); Assert.That(a.IsDisposed, Is.True); } + + [Test] + public void UnsuccessfulCompilation() + { + using Machine vm = new(); + CompilationResult result = vm.SetSource("5 +"); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.exception?.GetType(), Is.EqualTo(typeof(MissingTokenException))); + + if (vm.TrySetSource("5 + ", out Exception? exception)) + { + Assert.Fail(); + } + else + { + Assert.That(exception.GetType(), Is.EqualTo(typeof(MissingTokenException))); + } + } } }