From b005155868a1a2dfe16955a0159f54b8db4fd421 Mon Sep 17 00:00:00 2001 From: Phill Date: Wed, 9 Apr 2025 13:42:11 -0400 Subject: [PATCH 1/9] polish --- source/Machine.cs | 149 +++++++++++++++++++------------- source/Node.cs | 14 +++ source/TokenMap.cs | 29 ++++++- tests/ExpressionMachineTests.cs | 19 ++++ 4 files changed, 150 insertions(+), 61 deletions(-) diff --git a/source/Machine.cs b/source/Machine.cs index fe40b8d..638b889 100644 --- a/source/Machine.cs +++ b/source/Machine.cs @@ -1,5 +1,4 @@ -using Collections; -using Collections.Generic; +using Collections.Generic; using System; using System.Diagnostics; using System.Runtime.CompilerServices; @@ -24,7 +23,7 @@ public readonly ReadOnlySpan Source { MemoryAddress.ThrowIfDefault(machine); - return machine->source.AsSpan(); + return machine->source.GetSpan(machine->sourceLength); } } @@ -39,25 +38,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 +57,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 +68,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,6 +99,25 @@ 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() { @@ -120,13 +133,21 @@ public readonly void SetSource(ReadOnlySpan newSource) { 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(); machine->tree = Parsing.GetTree(machine->tokens.AsSpan()); } @@ -180,7 +201,7 @@ public readonly float GetVariable(ReadOnlySpan name) MemoryAddress.ThrowIfDefault(machine); ThrowIfVariableIsMissing(name); - int hash = new ASCIIText256(name).GetHashCode(); + long hash = name.GetLongHashCode(); int index = machine->variableNameHashes.IndexOf(hash); return machine->variableValues[index]; } @@ -210,7 +231,7 @@ public readonly bool ContainsVariable(ReadOnlySpan name) { MemoryAddress.ThrowIfDefault(machine); - int hash = new ASCIIText256(name).GetHashCode(); + long hash = name.GetLongHashCode(); return machine->variableNameHashes.Contains(hash); } @@ -239,7 +260,7 @@ public readonly bool ContainsFunction(ReadOnlySpan name) { MemoryAddress.ThrowIfDefault(machine); - int hash = new ASCIIText256(name).GetHashCode(); + long hash = name.GetLongHashCode(); return machine->functionNameHashes.Contains(hash); } @@ -250,7 +271,7 @@ public readonly bool ContainsFunction(ASCIIText256 name) { MemoryAddress.ThrowIfDefault(machine); - int hash = name.GetHashCode(); + long hash = name.GetLongHashCode(); return machine->functionNameHashes.Contains(hash); } @@ -261,7 +282,7 @@ public readonly void SetVariable(ReadOnlySpan name, float value) { MemoryAddress.ThrowIfDefault(machine); - int hash = new ASCIIText256(name).GetHashCode(); + long hash = name.GetLongHashCode(); if (machine->variableNameHashes.TryIndexOf(hash, out int index)) { machine->variableValues[index] = value; @@ -297,8 +318,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 +329,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,7 +341,7 @@ public readonly void SetFunction(ASCIIText256 name, Function function) { MemoryAddress.ThrowIfDefault(machine); - int hash = name.GetHashCode(); + long hash = name.GetLongHashCode(); if (machine->functionNameHashes.TryIndexOf(hash, out int index)) { machine->functionValues[index] = function; @@ -338,7 +361,7 @@ public readonly void SetFunction(ReadOnlySpan name, delegate* unmanagedfunctionNameHashes.TryIndexOf(hash, out int index)) { machine->functionValues[index] = f; @@ -376,7 +399,7 @@ public readonly void SetFunction(ReadOnlySpan name, Func fun MemoryAddress.ThrowIfDefault(machine); Function f = new(function); - int hash = new ASCIIText256(name).GetHashCode(); + long hash = name.GetLongHashCode(); if (machine->functionNameHashes.TryIndexOf(hash, out int index)) { machine->functionValues[index] = f; @@ -415,7 +438,7 @@ public readonly float InvokeFunction(ReadOnlySpan name, float value) MemoryAddress.ThrowIfDefault(machine); ThrowIfFunctionIsMissing(name); - int hash = new ASCIIText256(name).GetHashCode(); + long hash = name.GetLongHashCode(); int index = machine->functionNameHashes.IndexOf(hash); Function function = machine->functionValues[index]; return function.Invoke(value); @@ -430,7 +453,7 @@ public readonly float InvokeFunction(ASCIIText256 name, float value) MemoryAddress.ThrowIfDefault(machine); ThrowIfFunctionIsMissing(name); - int hash = name.GetHashCode(); + long hash = name.GetLongHashCode(); int index = machine->functionNameHashes.IndexOf(hash); Function function = machine->functionValues[index]; return function.Invoke(value); @@ -445,7 +468,7 @@ public readonly float InvokeFunction(string name, float value) MemoryAddress.ThrowIfDefault(machine); ThrowIfFunctionIsMissing(name); - int hash = new ASCIIText256(name).GetHashCode(); + long hash = name.GetLongHashCode(); int index = machine->functionNameHashes.IndexOf(hash); Function function = machine->functionValues[index]; return function.Invoke(value); @@ -458,7 +481,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,40 +495,46 @@ 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 List variableNameHashes; + public readonly List variableValues; + public readonly List functionNameHashes; + public readonly List 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(); + sourceCapacity = 4; + sourceLength = 0; + source = MemoryAddress.Allocate(sizeof(char) * sourceCapacity); + variableNameHashes = new(4); + variableValues = new(4); + functionNameHashes = new(4); + functionValues = new(4); + tokens = new(32); + tree = Node.Create(); } - /// - /// Allocates a new machine. - /// - public static Implementation* Allocate() + public Implementation(TokenMap map, ReadOnlySpan source) { - ref Implementation machine = ref MemoryAddress.Allocate(); - machine = new(new TokenMap()); - fixed (Implementation* pointer = &machine) - { - return pointer; - } + this.map = map; + sourceLength = source.Length; + sourceCapacity = Math.Max(4, sourceLength.GetNextPowerOf2()); + this.source = MemoryAddress.Allocate(sizeof(char) * sourceCapacity); + this.source.CopyFrom(source); + variableNameHashes = new(4); + variableValues = new(4); + functionNameHashes = new(4); + functionValues = new(4); + tokens = Parsing.GetTokens(source, map); + tree = Parsing.GetTree(tokens.AsSpan()); } /// diff --git a/source/Node.cs b/source/Node.cs index ac9d4cd..0a0c4e4 100644 --- a/source/Node.cs +++ b/source/Node.cs @@ -98,6 +98,12 @@ public Node(NodeType type, nint a, nint b, nint c) node = Implementation.Allocate(type, a, b, c); } + /// + public readonly override string ToString() + { + return Type.ToString(); + } + /// /// Disposes of the node. /// @@ -127,6 +133,14 @@ public readonly void Clear() node->c = default; } + /// + /// Creates an empty node. + /// + public static Node Create() + { + return new(Implementation.Allocate(default, default, default, default)); + } + /// /// Implementation type. /// 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..daee2e5 100644 --- a/tests/ExpressionMachineTests.cs +++ b/tests/ExpressionMachineTests.cs @@ -75,6 +75,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)); @@ -150,5 +152,22 @@ public void UseNodes() a.Dispose(); Assert.That(a.IsDisposed, Is.True); } + + [Test] + public void CompilationError() + { + //float? result = null; + //using Machine vm = new(); + //try + //{ + // vm.SetSource("5 +"); + //} + //catch (Exception ex) + //{ + // Assert.Fail(ex.Message); + //} + // + //result = vm.Evaluate(); + } } } From d41f2bd43d6e5f5e9d4b5d99c566cba7ae489647 Mon Sep 17 00:00:00 2001 From: Phill Date: Wed, 9 Apr 2025 13:51:54 -0400 Subject: [PATCH 2/9] parse nodes surface type instead of the pointer type --- source/Node.cs | 68 ++++++++++++++++++++++++++++------------------- source/Parsing.cs | 42 ++++++++++++++--------------- 2 files changed, 62 insertions(+), 48 deletions(-) diff --git a/source/Node.cs b/source/Node.cs index 0a0c4e4..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,18 @@ 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 . - /// - public Node(Implementation* pointer) - { - this.node = pointer; - } - /// /// Creates a new node with the given . /// public Node(NodeType type, nint a, nint b, nint c) { - node = Implementation.Allocate(type, a, b, c); + node = MemoryAddress.AllocatePointer(); + node[0] = new(type, a, b, c); } /// @@ -133,25 +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(Implementation.Allocate(default, default, default, default)); + 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; @@ -231,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..cef2047 100644 --- a/source/Parsing.cs +++ b/source/Parsing.cs @@ -89,32 +89,32 @@ public static void GetTokens(ReadOnlySpan expression, TokenMap map, List tokens) { int position = 0; - return new(TryParseExpression(ref position, tokens)); + return TryParseExpression(ref position, tokens); } - private static Node.Implementation* TryParseExpression(ref int position, ReadOnlySpan tokens) + private static Node TryParseExpression(ref int position, ReadOnlySpan tokens) { //todo: handle control nodes like if, else if, else, do, goto, and while - Node.Implementation* result = TryReadFactor(ref position, tokens); + Node 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)) + while (result != default && position < tokens.Length && IsTerm(current.type)) { 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); + Node right = TryReadFactor(ref position, tokens); + result = new(NodeType.Addition, result.Address, right.Address, default); } else if (current.type == Token.Type.Subtract) { position++; - Node.Implementation* right = TryReadFactor(ref position, tokens); - result = Node.Implementation.Allocate(NodeType.Subtraction, (nint)result, (nint)right, default); + Node right = TryReadFactor(ref position, tokens); + result = new(NodeType.Subtraction, result.Address, right.Address, default); } if (position == tokens.Length) @@ -128,28 +128,28 @@ public static Node GetTree(ReadOnlySpan tokens) return result; } - private static Node.Implementation* TryReadFactor(ref int position, ReadOnlySpan tokens) + private static Node TryReadFactor(ref int position, ReadOnlySpan tokens) { - Node.Implementation* factor = TryReadTerm(ref position, tokens); + Node factor = TryReadTerm(ref position, tokens); if (position == tokens.Length) { return factor; } Token current = tokens[position]; - while (factor is not null && position < tokens.Length && IsFactor(current.type)) + while (factor != default && position < tokens.Length && IsFactor(current.type)) { 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); + Node right = TryReadTerm(ref position, tokens); + factor = new(NodeType.Multiplication, factor.Address, right.Address, default); } else if (current.type == Token.Type.Divide) { position++; - Node.Implementation* right = TryReadTerm(ref position, tokens); - factor = Node.Implementation.Allocate(NodeType.Division, (nint)factor, (nint)right, default); + Node right = TryReadTerm(ref position, tokens); + factor = new(NodeType.Division, factor.Address, right.Address, default); } if (position == tokens.Length) @@ -163,7 +163,7 @@ public static Node GetTree(ReadOnlySpan tokens) return factor; } - private static Node.Implementation* TryReadTerm(ref int position, ReadOnlySpan tokens) + private static Node TryReadTerm(ref int position, ReadOnlySpan tokens) { if (position == tokens.Length) { @@ -175,7 +175,7 @@ public static Node GetTree(ReadOnlySpan tokens) position++; if (current.type == Token.Type.BeginGroup) { - Node.Implementation* term = TryParseExpression(ref position, tokens); + Node term = TryParseExpression(ref position, tokens); current = tokens[position]; if (current.type != Token.Type.EndGroup) { @@ -195,7 +195,7 @@ public static Node GetTree(ReadOnlySpan tokens) if (next.type == Token.Type.BeginGroup) { position++; - Node.Implementation* argument = TryParseExpression(ref position, tokens); + Node argument = TryParseExpression(ref position, tokens); if (position < tokens.Length) { current = tokens[position]; @@ -207,15 +207,15 @@ public static Node GetTree(ReadOnlySpan tokens) position++; } - return Node.Implementation.Allocate(NodeType.Call, start, length, (nint)argument); + return new(NodeType.Call, start, length, argument.Address); } } - return Node.Implementation.Allocate(NodeType.Value, start, length, default); + return new(NodeType.Value, start, length, default); } else { - return null; + return default; } } From 55a340af50dc204b64b467ac2ad453e9fb6f849b Mon Sep 17 00:00:00 2001 From: Phill Date: Wed, 9 Apr 2025 14:12:32 -0400 Subject: [PATCH 3/9] use a proper try pattern when parsing tree --- source/CompilationError.cs | 60 +++++++++++ source/Machine.cs | 10 +- source/Parsing.cs | 212 ++++++++++++++++++++++++------------- 3 files changed, 209 insertions(+), 73 deletions(-) create mode 100644 source/CompilationError.cs diff --git a/source/CompilationError.cs b/source/CompilationError.cs new file mode 100644 index 0000000..e3f22c5 --- /dev/null +++ b/source/CompilationError.cs @@ -0,0 +1,60 @@ +using System; +using Unmanaged; + +namespace ExpressionMachine +{ + /// + /// Represents a compilation error in the code. + /// + public readonly struct CompilationError + { + /// + /// Type of compilation error. + /// + public readonly Type type; + + /// + /// Additional error message. + /// + public readonly ASCIIText256 message; + + /// + /// Creates an instance. + /// + public CompilationError(Type type, ReadOnlySpan message) + { + this.type = type; + this.message = message; + } + + /// + public readonly override string ToString() + { + return $"{type}: {message}"; + } + + /// + /// Retrieves this error as an exception. + /// + public readonly Exception GetException() + { + return new(ToString()); + } + + /// + /// Defines all built-in compilation error types. + /// + public enum Type + { + /// + /// An additional token was expected but is missing. + /// + ExpectedAdditionalToken, + + /// + /// A group closing token was epxected but is missing. + /// + ExpectedGroupCloseToken + } + } +} \ No newline at end of file diff --git a/source/Machine.cs b/source/Machine.cs index 638b889..1f1139e 100644 --- a/source/Machine.cs +++ b/source/Machine.cs @@ -149,7 +149,10 @@ public readonly void SetSource(ReadOnlySpan newSource) //todo: efficiency: instead of disposing and creating a new instance, reuse it machine->tree.Dispose(); - machine->tree = Parsing.GetTree(machine->tokens.AsSpan()); + if (!Parsing.TryGetTree(machine->tokens.AsSpan(), out machine->tree, out CompilationError error)) + { + throw error.GetException(); + } } } @@ -534,7 +537,10 @@ public Implementation(TokenMap map, ReadOnlySpan source) functionNameHashes = new(4); functionValues = new(4); tokens = Parsing.GetTokens(source, map); - tree = Parsing.GetTree(tokens.AsSpan()); + if (!Parsing.TryGetTree(tokens.AsSpan(), out tree, out CompilationError error)) + { + throw error.GetException(); + } } /// diff --git a/source/Parsing.cs b/source/Parsing.cs index cef2047..ed93b05 100644 --- a/source/Parsing.cs +++ b/source/Parsing.cs @@ -1,5 +1,4 @@ -using Collections; -using Collections.Generic; +using Collections.Generic; using System; namespace ExpressionMachine @@ -86,104 +85,159 @@ public static void GetTokens(ReadOnlySpan 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, out CompilationError error) { int position = 0; - return TryParseExpression(ref position, tokens); + return TryParseExpression(ref position, tokens, out node, out error); } - private static Node TryParseExpression(ref int position, ReadOnlySpan tokens) + private static bool TryParseExpression(ref int position, ReadOnlySpan tokens, out Node node, out CompilationError error) { //todo: handle control nodes like if, else if, else, do, goto, and while - Node result = TryReadFactor(ref position, tokens); - if (position == tokens.Length) - { - return result; - } - - Token current = tokens[position]; - while (result != default && position < tokens.Length && IsTerm(current.type)) + if (TryReadFactor(ref position, tokens, out node, out error)) { - if (current.type == Token.Type.Add) - { - position++; - Node right = TryReadFactor(ref position, tokens); - result = new(NodeType.Addition, result.Address, right.Address, default); - } - else if (current.type == Token.Type.Subtract) + if (position == tokens.Length) { - position++; - Node right = TryReadFactor(ref position, tokens); - result = new(NodeType.Subtraction, result.Address, right.Address, 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 error)) + { + node = new(NodeType.Addition, node.Address, right.Address, default); + } + else + { + node = default; + return false; + } + } + else if (current.type == Token.Type.Subtract) + { + position++; + if (TryReadFactor(ref position, tokens, out Node right, out error)) + { + node = new(NodeType.Subtraction, node.Address, right.Address, default); + } + else + { + node = default; + return false; + } + } - current = tokens[position]; - } + if (position == tokens.Length) + { + break; + } - return result; - } + current = tokens[position]; + } - private static Node TryReadFactor(ref int position, ReadOnlySpan tokens) - { - Node factor = TryReadTerm(ref position, tokens); - if (position == tokens.Length) + return true; + } + else { - return factor; + node = default; + return false; } + } - Token current = tokens[position]; - while (factor != default && position < tokens.Length && IsFactor(current.type)) + private static bool TryReadFactor(ref int position, ReadOnlySpan tokens, out Node node, out CompilationError error) + { + if (TryReadTerm(ref position, tokens, out node, out error)) { - if (current.type == Token.Type.Multiply) - { - position++; - Node right = TryReadTerm(ref position, tokens); - factor = new(NodeType.Multiplication, factor.Address, right.Address, default); - } - else if (current.type == Token.Type.Divide) + if (position == tokens.Length) { - position++; - Node right = TryReadTerm(ref position, tokens); - factor = new(NodeType.Division, factor.Address, right.Address, 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 error)) + { + node = new(NodeType.Multiplication, node.Address, right.Address, default); + } + else + { + node = default; + return false; + } + } + else if (current.type == Token.Type.Divide) + { + position++; + if (TryReadTerm(ref position, tokens, out Node right, out error)) + { + node = new(NodeType.Division, node.Address, right.Address, default); + } + else + { + 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 TryReadTerm(ref int position, ReadOnlySpan tokens) + private static bool TryReadTerm(ref int position, ReadOnlySpan tokens, out Node node, out CompilationError error) { if (position == tokens.Length) { Token lastToken = tokens[position - 1]; - throw new FormatException($"Expected a token after {lastToken}"); + node = default; + error = new(CompilationError.Type.ExpectedAdditionalToken, $"Expected a token after `{lastToken.type}`"); + return false; } Token current = tokens[position]; position++; if (current.type == Token.Type.BeginGroup) { - Node term = TryParseExpression(ref position, tokens); - current = tokens[position]; - if (current.type != Token.Type.EndGroup) + if (TryParseExpression(ref position, tokens, out Node term, out error)) { - throw new FormatException("Expected closing parenthesis"); - } + current = tokens[position]; + if (current.type != Token.Type.EndGroup) + { + term.Dispose(); + node = default; + error = new(CompilationError.Type.ExpectedGroupCloseToken, $"Expected a `` to close the start of a group"); + return false; + } - position++; - return term; + position++; + node = term; + return true; + } + else + { + node = default; + return false; + } } else if (current.type == Token.Type.Value) { @@ -195,27 +249,43 @@ private static Node TryReadTerm(ref int position, ReadOnlySpan tokens) if (next.type == Token.Type.BeginGroup) { position++; - Node argument = TryParseExpression(ref position, tokens); - if (position < tokens.Length) + if (TryParseExpression(ref position, tokens, out Node argument, out error)) { - 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; + error = new(CompilationError.Type.ExpectedGroupCloseToken, $"Expected a `` to close the start of a group"); + return false; + } + + position++; } - position++; + node = new(NodeType.Call, start, length, argument.Address); + error = default; + return true; + } + else + { + node = default; + return false; } - - return new(NodeType.Call, start, length, argument.Address); } } - return new(NodeType.Value, start, length, default); + node = new(NodeType.Value, start, length, default); + error = default; + return true; } else { - return default; + node = default; + error = default; + return true; } } From cf97f5ab97a0ef616a8cbdbbd8787f304302b0f2 Mon Sep 17 00:00:00 2001 From: Phill Date: Wed, 9 Apr 2025 14:24:18 -0400 Subject: [PATCH 4/9] catch compilation errors #1 --- source/CompilationError.cs | 5 +++ source/Machine.cs | 58 ++++++++++++++++++++++++++++++++- source/Parsing.cs | 17 ++++++---- tests/ExpressionMachineTests.cs | 18 +++------- 4 files changed, 78 insertions(+), 20 deletions(-) diff --git a/source/CompilationError.cs b/source/CompilationError.cs index e3f22c5..3e7baa0 100644 --- a/source/CompilationError.cs +++ b/source/CompilationError.cs @@ -46,6 +46,11 @@ public readonly Exception GetException() /// public enum Type { + /// + /// Not an error. + /// + None, + /// /// An additional token was expected but is missing. /// diff --git a/source/Machine.cs b/source/Machine.cs index 1f1139e..57e6cd5 100644 --- a/source/Machine.cs +++ b/source/Machine.cs @@ -129,7 +129,7 @@ public void Dispose() /// /// Assigns to the machine. /// - public readonly void SetSource(ReadOnlySpan newSource) + public readonly bool TrySetSource(ReadOnlySpan newSource, out CompilationError error) { MemoryAddress.ThrowIfDefault(machine); @@ -148,14 +148,70 @@ public readonly void SetSource(ReadOnlySpan newSource) 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 error)) + { + return true; + } + else + { + machine->tree = Node.Create(); + return false; + } + } + + error = default; + return true; + } + + /// + /// Assigns to the machine. + /// + public readonly void 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(); if (!Parsing.TryGetTree(machine->tokens.AsSpan(), out machine->tree, out CompilationError error)) { + machine->tree = Node.Create(); throw error.GetException(); } } } + /// + /// Assigns to the machine. + /// + public readonly bool TrySetSource(ASCIIText256 newSource, out CompilationError error) + { + Span nameSpan = stackalloc char[newSource.Length]; + newSource.CopyTo(nameSpan); + return TrySetSource(nameSpan, out error); + } + + /// + /// Assigns to the machine. + /// + public readonly bool TrySetSource(string newSource, out CompilationError error) + { + return TrySetSource(newSource.AsSpan(), out error); + } + /// /// Assigns to the machine. /// diff --git a/source/Parsing.cs b/source/Parsing.cs index ed93b05..2ae79d5 100644 --- a/source/Parsing.cs +++ b/source/Parsing.cs @@ -44,16 +44,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)); @@ -66,7 +68,7 @@ public static void GetTokens(ReadOnlySpan expression, TokenMap map, List tok } else { + node.Dispose(); node = default; return false; } @@ -126,6 +129,7 @@ private static bool TryParseExpression(ref int position, ReadOnlySpan tok } else { + node.Dispose(); node = default; return false; } @@ -169,6 +173,7 @@ private static bool TryReadFactor(ref int position, ReadOnlySpan tokens, } else { + node.Dispose(); node = default; return false; } @@ -182,6 +187,7 @@ private static bool TryReadFactor(ref int position, ReadOnlySpan tokens, } else { + node.Dispose(); node = default; return false; } @@ -218,19 +224,18 @@ private static bool TryReadTerm(ref int position, ReadOnlySpan tokens, ou position++; if (current.type == Token.Type.BeginGroup) { - if (TryParseExpression(ref position, tokens, out Node term, out error)) + if (TryParseExpression(ref position, tokens, out node, out error)) { current = tokens[position]; if (current.type != Token.Type.EndGroup) { - term.Dispose(); + node.Dispose(); node = default; error = new(CompilationError.Type.ExpectedGroupCloseToken, $"Expected a `` to close the start of a group"); return false; } position++; - node = term; return true; } else diff --git a/tests/ExpressionMachineTests.cs b/tests/ExpressionMachineTests.cs index daee2e5..5a67b99 100644 --- a/tests/ExpressionMachineTests.cs +++ b/tests/ExpressionMachineTests.cs @@ -154,20 +154,12 @@ public void UseNodes() } [Test] - public void CompilationError() + public void UnsuccessfulCompilation() { - //float? result = null; - //using Machine vm = new(); - //try - //{ - // vm.SetSource("5 +"); - //} - //catch (Exception ex) - //{ - // Assert.Fail(ex.Message); - //} - // - //result = vm.Evaluate(); + using Machine vm = new(); + bool success = vm.TrySetSource("5 +", out CompilationError error); + Assert.That(success, Is.False); + Assert.That(error.type, Is.EqualTo(CompilationError.Type.ExpectedAdditionalToken)); } } } From ee15fdc406dc1e3010bb96f5ad1fbefc9de15f0f Mon Sep 17 00:00:00 2001 From: Phill Date: Wed, 9 Apr 2025 14:44:14 -0400 Subject: [PATCH 5/9] compilation results --- source/CompilationError.cs | 31 ++--------- source/CompilationResult.cs | 99 +++++++++++++++++++++++++++++++++ source/Machine.cs | 15 +++-- source/Parsing.cs | 6 +- tests/ExpressionMachineTests.cs | 6 +- 5 files changed, 119 insertions(+), 38 deletions(-) create mode 100644 source/CompilationResult.cs diff --git a/source/CompilationError.cs b/source/CompilationError.cs index 3e7baa0..90128a1 100644 --- a/source/CompilationError.cs +++ b/source/CompilationError.cs @@ -11,26 +11,26 @@ public readonly struct CompilationError /// /// Type of compilation error. /// - public readonly Type type; + public readonly CompilationResult.Type type; /// /// Additional error message. /// - public readonly ASCIIText256 message; + public readonly ASCIIText256 errorMessage; /// /// Creates an instance. /// - public CompilationError(Type type, ReadOnlySpan message) + public CompilationError(CompilationResult.Type type, ReadOnlySpan errorMessage) { this.type = type; - this.message = message; + this.errorMessage = errorMessage; } /// public readonly override string ToString() { - return $"{type}: {message}"; + return $"{type}: {errorMessage}"; } /// @@ -40,26 +40,5 @@ public readonly Exception GetException() { return new(ToString()); } - - /// - /// Defines all built-in compilation error types. - /// - public enum Type - { - /// - /// Not an error. - /// - None, - - /// - /// An additional token was expected but is missing. - /// - ExpectedAdditionalToken, - - /// - /// A group closing token was epxected but is missing. - /// - ExpectedGroupCloseToken - } } } \ No newline at end of file diff --git a/source/CompilationResult.cs b/source/CompilationResult.cs new file mode 100644 index 0000000..46da4d7 --- /dev/null +++ b/source/CompilationResult.cs @@ -0,0 +1,99 @@ +using System; +using Unmanaged; + +namespace ExpressionMachine +{ + /// + /// Represents the result of compilation. + /// + public readonly struct CompilationResult : IEquatable + { + /// + /// Success compilation result. + /// + public static CompilationResult Success => new(Type.Success); + + /// + /// Result type. + /// + public readonly Type type; + + /// + /// Error message if compilation wasn't successful. + /// + public readonly ASCIIText256 errorMessage; + + /// + /// Checks if the compilation was successful. + /// + public readonly bool IsSuccess => type == Type.Success; + + internal CompilationResult(CompilationError error) + { + type = error.type; + errorMessage = error.errorMessage; + } + + private CompilationResult(Type type) + { + this.type = type; + errorMessage = default; + } + + /// + public readonly override bool Equals(object? obj) + { + return obj is CompilationResult result && Equals(result); + } + + /// + public readonly bool Equals(CompilationResult other) + { + return type == other.type && errorMessage.Equals(other.errorMessage); + } + + /// + public readonly override int GetHashCode() + { + return HashCode.Combine(type, errorMessage); + } + + /// + public static bool operator ==(CompilationResult left, CompilationResult right) + { + return left.Equals(right); + } + + /// + public static bool operator !=(CompilationResult left, CompilationResult right) + { + return !(left == right); + } + + /// + /// Defines all built-in compilation error types. + /// + public enum Type + { + /// + /// Default value. + /// + None, + + /// + /// Compilation successful. + /// + Success, + + /// + /// An additional token was expected but is missing. + /// + ExpectedAdditionalToken, + + /// + /// A group closing token was epxected but is missing. + /// + ExpectedGroupCloseToken + } + } +} \ No newline at end of file diff --git a/source/Machine.cs b/source/Machine.cs index 57e6cd5..2acb5f2 100644 --- a/source/Machine.cs +++ b/source/Machine.cs @@ -167,7 +167,7 @@ public readonly bool TrySetSource(ReadOnlySpan newSource, out CompilationE /// /// Assigns to the machine. /// - public readonly void SetSource(ReadOnlySpan newSource) + public readonly CompilationResult SetSource(ReadOnlySpan newSource) { MemoryAddress.ThrowIfDefault(machine); @@ -189,9 +189,11 @@ public readonly void SetSource(ReadOnlySpan newSource) if (!Parsing.TryGetTree(machine->tokens.AsSpan(), out machine->tree, out CompilationError error)) { machine->tree = Node.Create(); - throw error.GetException(); + return new(error); } } + + return CompilationResult.Success; } /// @@ -215,19 +217,19 @@ public readonly bool TrySetSource(string newSource, out CompilationError error) /// /// Assigns to the machine. /// - public readonly void SetSource(ASCIIText256 newSource) + public readonly CompilationResult SetSource(ASCIIText256 newSource) { Span nameSpan = stackalloc char[newSource.Length]; newSource.CopyTo(nameSpan); - SetSource(nameSpan); + return SetSource(nameSpan); } /// /// Assigns to the machine. /// - public readonly void SetSource(string newSource) + public readonly CompilationResult SetSource(string newSource) { - SetSource(newSource.AsSpan()); + return SetSource(newSource.AsSpan()); } /// @@ -595,6 +597,7 @@ public Implementation(TokenMap map, ReadOnlySpan source) tokens = Parsing.GetTokens(source, map); if (!Parsing.TryGetTree(tokens.AsSpan(), out tree, out CompilationError error)) { + tree = Node.Create(); throw error.GetException(); } } diff --git a/source/Parsing.cs b/source/Parsing.cs index 2ae79d5..0f62662 100644 --- a/source/Parsing.cs +++ b/source/Parsing.cs @@ -216,7 +216,7 @@ private static bool TryReadTerm(ref int position, ReadOnlySpan tokens, ou { Token lastToken = tokens[position - 1]; node = default; - error = new(CompilationError.Type.ExpectedAdditionalToken, $"Expected a token after `{lastToken.type}`"); + error = new(CompilationResult.Type.ExpectedAdditionalToken, $"Expected a token after `{lastToken.type}`"); return false; } @@ -231,7 +231,7 @@ private static bool TryReadTerm(ref int position, ReadOnlySpan tokens, ou { node.Dispose(); node = default; - error = new(CompilationError.Type.ExpectedGroupCloseToken, $"Expected a `` to close the start of a group"); + error = new(CompilationResult.Type.ExpectedGroupCloseToken, $"Expected a `` to close the start of a group"); return false; } @@ -263,7 +263,7 @@ private static bool TryReadTerm(ref int position, ReadOnlySpan tokens, ou { argument.Dispose(); node = default; - error = new(CompilationError.Type.ExpectedGroupCloseToken, $"Expected a `` to close the start of a group"); + error = new(CompilationResult.Type.ExpectedGroupCloseToken, $"Expected a `` to close the start of a group"); return false; } diff --git a/tests/ExpressionMachineTests.cs b/tests/ExpressionMachineTests.cs index 5a67b99..2017814 100644 --- a/tests/ExpressionMachineTests.cs +++ b/tests/ExpressionMachineTests.cs @@ -157,9 +157,9 @@ public void UseNodes() public void UnsuccessfulCompilation() { using Machine vm = new(); - bool success = vm.TrySetSource("5 +", out CompilationError error); - Assert.That(success, Is.False); - Assert.That(error.type, Is.EqualTo(CompilationError.Type.ExpectedAdditionalToken)); + CompilationResult result = vm.SetSource("5 +"); + Assert.That(result.IsSuccess, Is.False); + Assert.That(result.type, Is.EqualTo(CompilationResult.Type.ExpectedAdditionalToken)); } } } From 3da1bd6b0505461de4f8032df8a62b58ca17cba0 Mon Sep 17 00:00:00 2001 From: Phill Date: Wed, 9 Apr 2025 14:52:01 -0400 Subject: [PATCH 6/9] use exceptions to report compilation issues --- source/CompilationError.cs | 44 ----------------- source/CompilationResult.cs | 55 ++++------------------ source/Exceptions/MissingTokenException.cs | 20 ++++++++ source/Machine.cs | 23 ++++----- source/Parsing.cs | 39 +++++++-------- tests/ExpressionMachineTests.cs | 2 +- 6 files changed, 61 insertions(+), 122 deletions(-) delete mode 100644 source/CompilationError.cs create mode 100644 source/Exceptions/MissingTokenException.cs diff --git a/source/CompilationError.cs b/source/CompilationError.cs deleted file mode 100644 index 90128a1..0000000 --- a/source/CompilationError.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using Unmanaged; - -namespace ExpressionMachine -{ - /// - /// Represents a compilation error in the code. - /// - public readonly struct CompilationError - { - /// - /// Type of compilation error. - /// - public readonly CompilationResult.Type type; - - /// - /// Additional error message. - /// - public readonly ASCIIText256 errorMessage; - - /// - /// Creates an instance. - /// - public CompilationError(CompilationResult.Type type, ReadOnlySpan errorMessage) - { - this.type = type; - this.errorMessage = errorMessage; - } - - /// - public readonly override string ToString() - { - return $"{type}: {errorMessage}"; - } - - /// - /// Retrieves this error as an exception. - /// - public readonly Exception GetException() - { - return new(ToString()); - } - } -} \ No newline at end of file diff --git a/source/CompilationResult.cs b/source/CompilationResult.cs index 46da4d7..78ae573 100644 --- a/source/CompilationResult.cs +++ b/source/CompilationResult.cs @@ -1,5 +1,4 @@ using System; -using Unmanaged; namespace ExpressionMachine { @@ -11,33 +10,21 @@ namespace ExpressionMachine /// /// Success compilation result. /// - public static CompilationResult Success => new(Type.Success); + public static CompilationResult Success => new(null); /// - /// Result type. + /// Compilation exception if there was one. /// - public readonly Type type; - - /// - /// Error message if compilation wasn't successful. - /// - public readonly ASCIIText256 errorMessage; + public readonly Exception? exception; /// /// Checks if the compilation was successful. /// - public readonly bool IsSuccess => type == Type.Success; + public readonly bool IsSuccess => exception is null; - internal CompilationResult(CompilationError error) + internal CompilationResult(Exception? exception) { - type = error.type; - errorMessage = error.errorMessage; - } - - private CompilationResult(Type type) - { - this.type = type; - errorMessage = default; + this.exception = exception; } /// @@ -49,13 +36,13 @@ public readonly override bool Equals(object? obj) /// public readonly bool Equals(CompilationResult other) { - return type == other.type && errorMessage.Equals(other.errorMessage); + return exception == other.exception; } /// public readonly override int GetHashCode() { - return HashCode.Combine(type, errorMessage); + return exception?.GetHashCode() ?? 0; } /// @@ -69,31 +56,5 @@ public readonly override int GetHashCode() { return !(left == right); } - - /// - /// Defines all built-in compilation error types. - /// - public enum Type - { - /// - /// Default value. - /// - None, - - /// - /// Compilation successful. - /// - Success, - - /// - /// An additional token was expected but is missing. - /// - ExpectedAdditionalToken, - - /// - /// A group closing token was epxected but is missing. - /// - ExpectedGroupCloseToken - } } } \ 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/Machine.cs b/source/Machine.cs index 2acb5f2..6e0397d 100644 --- a/source/Machine.cs +++ b/source/Machine.cs @@ -1,6 +1,7 @@ using Collections.Generic; using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using Unmanaged; @@ -129,7 +130,7 @@ public void Dispose() /// /// Assigns to the machine. /// - public readonly bool TrySetSource(ReadOnlySpan newSource, out CompilationError error) + public readonly bool TrySetSource(ReadOnlySpan newSource, [NotNullWhen(false)] out Exception? exception) { MemoryAddress.ThrowIfDefault(machine); @@ -149,7 +150,7 @@ public readonly bool TrySetSource(ReadOnlySpan newSource, out CompilationE //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 error)) + if (Parsing.TryGetTree(machine->tokens.AsSpan(), out machine->tree, out exception)) { return true; } @@ -160,7 +161,7 @@ public readonly bool TrySetSource(ReadOnlySpan newSource, out CompilationE } } - error = default; + exception = default; return true; } @@ -186,10 +187,10 @@ public readonly CompilationResult SetSource(ReadOnlySpan newSource) Parsing.GetTokens(newSource, machine->map, machine->tokens); machine->tree.Dispose(); - if (!Parsing.TryGetTree(machine->tokens.AsSpan(), out machine->tree, out CompilationError error)) + if (!Parsing.TryGetTree(machine->tokens.AsSpan(), out machine->tree, out Exception? exception)) { machine->tree = Node.Create(); - return new(error); + return new(exception); } } @@ -199,19 +200,19 @@ public readonly CompilationResult SetSource(ReadOnlySpan newSource) /// /// Assigns to the machine. /// - public readonly bool TrySetSource(ASCIIText256 newSource, out CompilationError error) + public readonly bool TrySetSource(ASCIIText256 newSource, [NotNullWhen(false)] out Exception? exception) { Span nameSpan = stackalloc char[newSource.Length]; newSource.CopyTo(nameSpan); - return TrySetSource(nameSpan, out error); + return TrySetSource(nameSpan, out exception); } /// /// Assigns to the machine. /// - public readonly bool TrySetSource(string newSource, out CompilationError error) + public readonly bool TrySetSource(string newSource, [NotNullWhen(false)] out Exception? exception) { - return TrySetSource(newSource.AsSpan(), out error); + return TrySetSource(newSource.AsSpan(), out exception); } /// @@ -595,10 +596,10 @@ public Implementation(TokenMap map, ReadOnlySpan source) functionNameHashes = new(4); functionValues = new(4); tokens = Parsing.GetTokens(source, map); - if (!Parsing.TryGetTree(tokens.AsSpan(), out tree, out CompilationError error)) + if (!Parsing.TryGetTree(tokens.AsSpan(), out tree, out Exception? exception)) { tree = Node.Create(); - throw error.GetException(); + throw exception; } } diff --git a/source/Parsing.cs b/source/Parsing.cs index 0f62662..51b1748 100644 --- a/source/Parsing.cs +++ b/source/Parsing.cs @@ -1,5 +1,6 @@ using Collections.Generic; using System; +using System.Diagnostics.CodeAnalysis; namespace ExpressionMachine { @@ -87,16 +88,16 @@ public static void GetTokens(ReadOnlySpan expression, TokenMap map, List containing the expression represented /// by the given . /// - public static bool TryGetTree(ReadOnlySpan tokens, out Node node, out CompilationError error) + public static bool TryGetTree(ReadOnlySpan tokens, out Node node, [NotNullWhen(false)] out Exception? exception) { int position = 0; - return TryParseExpression(ref position, tokens, out node, out error); + return TryParseExpression(ref position, tokens, out node, out exception); } - private static bool TryParseExpression(ref int position, ReadOnlySpan tokens, out Node node, out CompilationError error) + 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 - if (TryReadFactor(ref position, tokens, out node, out error)) + if (TryReadFactor(ref position, tokens, out node, out exception)) { if (position == tokens.Length) { @@ -109,7 +110,7 @@ private static bool TryParseExpression(ref int position, ReadOnlySpan tok if (current.type == Token.Type.Add) { position++; - if (TryReadFactor(ref position, tokens, out Node right, out error)) + if (TryReadFactor(ref position, tokens, out Node right, out exception)) { node = new(NodeType.Addition, node.Address, right.Address, default); } @@ -123,7 +124,7 @@ private static bool TryParseExpression(ref int position, ReadOnlySpan tok else if (current.type == Token.Type.Subtract) { position++; - if (TryReadFactor(ref position, tokens, out Node right, out error)) + if (TryReadFactor(ref position, tokens, out Node right, out exception)) { node = new(NodeType.Subtraction, node.Address, right.Address, default); } @@ -152,9 +153,9 @@ private static bool TryParseExpression(ref int position, ReadOnlySpan tok } } - private static bool TryReadFactor(ref int position, ReadOnlySpan tokens, out Node node, out CompilationError error) + 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 error)) + if (TryReadTerm(ref position, tokens, out node, out exception)) { if (position == tokens.Length) { @@ -167,7 +168,7 @@ private static bool TryReadFactor(ref int position, ReadOnlySpan tokens, if (current.type == Token.Type.Multiply) { position++; - if (TryReadTerm(ref position, tokens, out Node right, out error)) + if (TryReadTerm(ref position, tokens, out Node right, out exception)) { node = new(NodeType.Multiplication, node.Address, right.Address, default); } @@ -181,7 +182,7 @@ private static bool TryReadFactor(ref int position, ReadOnlySpan tokens, else if (current.type == Token.Type.Divide) { position++; - if (TryReadTerm(ref position, tokens, out Node right, out error)) + if (TryReadTerm(ref position, tokens, out Node right, out exception)) { node = new(NodeType.Division, node.Address, right.Address, default); } @@ -210,13 +211,13 @@ private static bool TryReadFactor(ref int position, ReadOnlySpan tokens, } } - private static bool TryReadTerm(ref int position, ReadOnlySpan tokens, out Node node, out CompilationError error) + 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]; node = default; - error = new(CompilationResult.Type.ExpectedAdditionalToken, $"Expected a token after `{lastToken.type}`"); + exception = new MissingTokenException(); return false; } @@ -224,14 +225,14 @@ private static bool TryReadTerm(ref int position, ReadOnlySpan tokens, ou position++; if (current.type == Token.Type.BeginGroup) { - if (TryParseExpression(ref position, tokens, out node, out error)) + if (TryParseExpression(ref position, tokens, out node, out exception)) { current = tokens[position]; if (current.type != Token.Type.EndGroup) { node.Dispose(); node = default; - error = new(CompilationResult.Type.ExpectedGroupCloseToken, $"Expected a `` to close the start of a group"); + exception = new MissingGroupCloseToken(); return false; } @@ -254,7 +255,7 @@ private static bool TryReadTerm(ref int position, ReadOnlySpan tokens, ou if (next.type == Token.Type.BeginGroup) { position++; - if (TryParseExpression(ref position, tokens, out Node argument, out error)) + if (TryParseExpression(ref position, tokens, out Node argument, out exception)) { if (position < tokens.Length) { @@ -263,7 +264,7 @@ private static bool TryReadTerm(ref int position, ReadOnlySpan tokens, ou { argument.Dispose(); node = default; - error = new(CompilationResult.Type.ExpectedGroupCloseToken, $"Expected a `` to close the start of a group"); + exception = new MissingGroupCloseToken(); return false; } @@ -271,7 +272,7 @@ private static bool TryReadTerm(ref int position, ReadOnlySpan tokens, ou } node = new(NodeType.Call, start, length, argument.Address); - error = default; + exception = null; return true; } else @@ -283,13 +284,13 @@ private static bool TryReadTerm(ref int position, ReadOnlySpan tokens, ou } node = new(NodeType.Value, start, length, default); - error = default; + exception = null; return true; } else { node = default; - error = default; + exception = null; return true; } } diff --git a/tests/ExpressionMachineTests.cs b/tests/ExpressionMachineTests.cs index 2017814..50a7a34 100644 --- a/tests/ExpressionMachineTests.cs +++ b/tests/ExpressionMachineTests.cs @@ -159,7 +159,7 @@ public void UnsuccessfulCompilation() using Machine vm = new(); CompilationResult result = vm.SetSource("5 +"); Assert.That(result.IsSuccess, Is.False); - Assert.That(result.type, Is.EqualTo(CompilationResult.Type.ExpectedAdditionalToken)); + Assert.That(result.exception?.GetType(), Is.EqualTo(typeof(MissingTokenException))); } } } From 454d2415f99d3ab62adf8c2bb72ac308ee52831c Mon Sep 17 00:00:00 2001 From: Phill Date: Wed, 9 Apr 2025 15:01:51 -0400 Subject: [PATCH 7/9] use dictionaries for look ups --- source/Machine.cs | 116 ++++++++++++++++------------------------------ 1 file changed, 41 insertions(+), 75 deletions(-) diff --git a/source/Machine.cs b/source/Machine.cs index 6e0397d..87618e4 100644 --- a/source/Machine.cs +++ b/source/Machine.cs @@ -124,7 +124,18 @@ 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); } /// @@ -240,7 +251,6 @@ public readonly void ClearVariables() { MemoryAddress.ThrowIfDefault(machine); - machine->variableNameHashes.Clear(); machine->variableValues.Clear(); } @@ -251,7 +261,6 @@ public readonly void ClearFunctions() { MemoryAddress.ThrowIfDefault(machine); - machine->functionNameHashes.Clear(); machine->functionValues.Clear(); } @@ -264,8 +273,7 @@ public readonly float GetVariable(ReadOnlySpan name) ThrowIfVariableIsMissing(name); long hash = name.GetLongHashCode(); - int index = machine->variableNameHashes.IndexOf(hash); - return machine->variableValues[index]; + return machine->variableValues[hash]; } /// @@ -294,7 +302,7 @@ public readonly bool ContainsVariable(ReadOnlySpan name) MemoryAddress.ThrowIfDefault(machine); long hash = name.GetLongHashCode(); - return machine->variableNameHashes.Contains(hash); + return machine->variableValues.ContainsKey(hash); } /// @@ -323,7 +331,7 @@ public readonly bool ContainsFunction(ReadOnlySpan name) MemoryAddress.ThrowIfDefault(machine); long hash = name.GetLongHashCode(); - return machine->functionNameHashes.Contains(hash); + return machine->functionValues.ContainsKey(hash); } /// @@ -334,7 +342,7 @@ public readonly bool ContainsFunction(ASCIIText256 name) MemoryAddress.ThrowIfDefault(machine); long hash = name.GetLongHashCode(); - return machine->functionNameHashes.Contains(hash); + return machine->functionValues.ContainsKey(hash); } /// @@ -345,15 +353,13 @@ public readonly void SetVariable(ReadOnlySpan name, float value) MemoryAddress.ThrowIfDefault(machine); long hash = name.GetLongHashCode(); - if (machine->variableNameHashes.TryIndexOf(hash, out int index)) + 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; } /// @@ -404,15 +410,13 @@ public readonly void SetFunction(ASCIIText256 name, Function function) MemoryAddress.ThrowIfDefault(machine); long hash = name.GetLongHashCode(); - if (machine->functionNameHashes.TryIndexOf(hash, out int index)) + 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; } /// @@ -422,17 +426,14 @@ public readonly void SetFunction(ReadOnlySpan name, delegate* unmanagedfunctionNameHashes.TryIndexOf(hash, out int index)) - { - machine->functionValues[index] = f; - } - else + 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); } /// @@ -460,17 +461,14 @@ public readonly void SetFunction(ReadOnlySpan name, Func fun { MemoryAddress.ThrowIfDefault(machine); - Function f = new(function); long hash = name.GetLongHashCode(); - if (machine->functionNameHashes.TryIndexOf(hash, out int index)) - { - machine->functionValues[index] = f; - } - else + 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); } /// @@ -501,8 +499,7 @@ public readonly float InvokeFunction(ReadOnlySpan name, float value) ThrowIfFunctionIsMissing(name); long hash = name.GetLongHashCode(); - int index = machine->functionNameHashes.IndexOf(hash); - Function function = machine->functionValues[index]; + Function function = machine->functionValues[hash]; return function.Invoke(value); } @@ -516,8 +513,7 @@ public readonly float InvokeFunction(ASCIIText256 name, float value) ThrowIfFunctionIsMissing(name); long hash = name.GetLongHashCode(); - int index = machine->functionNameHashes.IndexOf(hash); - Function function = machine->functionValues[index]; + Function function = machine->functionValues[hash]; return function.Invoke(value); } @@ -531,8 +527,7 @@ public readonly float InvokeFunction(string name, float value) ThrowIfFunctionIsMissing(name); long hash = name.GetLongHashCode(); - int index = machine->functionNameHashes.IndexOf(hash); - Function function = machine->functionValues[index]; + Function function = machine->functionValues[hash]; return function.Invoke(value); } @@ -564,10 +559,8 @@ internal struct Implementation public int sourceLength; public int sourceCapacity; public readonly TokenMap map; - public readonly List variableNameHashes; - public readonly List variableValues; - public readonly List functionNameHashes; - public readonly List functionValues; + public readonly Dictionary variableValues; + public readonly Dictionary functionValues; public readonly List tokens; public Implementation(TokenMap map) @@ -576,9 +569,7 @@ public Implementation(TokenMap map) sourceCapacity = 4; sourceLength = 0; source = MemoryAddress.Allocate(sizeof(char) * sourceCapacity); - variableNameHashes = new(4); variableValues = new(4); - functionNameHashes = new(4); functionValues = new(4); tokens = new(32); tree = Node.Create(); @@ -591,9 +582,7 @@ public Implementation(TokenMap map, ReadOnlySpan source) sourceCapacity = Math.Max(4, sourceLength.GetNextPowerOf2()); this.source = MemoryAddress.Allocate(sizeof(char) * sourceCapacity); this.source.CopyFrom(source); - variableNameHashes = new(4); variableValues = new(4); - functionNameHashes = new(4); functionValues = new(4); tokens = Parsing.GetTokens(source, map); if (!Parsing.TryGetTree(tokens.AsSpan(), out tree, out Exception? exception)) @@ -602,29 +591,6 @@ public Implementation(TokenMap map, ReadOnlySpan source) throw exception; } } - - /// - /// Frees the given . - /// - public static void Free(ref Implementation* machine) - { - MemoryAddress.ThrowIfDefault(machine); - - for (int i = 0; i < machine->functionValues.Count; i++) - { - machine->functionValues[i].Dispose(); - } - - 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); - } } } } \ No newline at end of file From 89f0db82a1758f0e8c96080341be045ba2457cb9 Mon Sep 17 00:00:00 2001 From: Phill Date: Wed, 9 Apr 2025 15:06:11 -0400 Subject: [PATCH 8/9] update nuget package settings --- source/ExpressionMachine.csproj | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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 + \ + + + From 8afcf3093e34b7a43ac5e70ae7e391a803a5116f Mon Sep 17 00:00:00 2001 From: Phill Date: Wed, 9 Apr 2025 15:16:45 -0400 Subject: [PATCH 9/9] update docs and verify with test --- README.md | 37 ++++++++++++++++++--------------- tests/ExpressionMachineTests.cs | 32 +++++++++++++++++----------- 2 files changed, 40 insertions(+), 29 deletions(-) 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/tests/ExpressionMachineTests.cs b/tests/ExpressionMachineTests.cs index 50a7a34..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; @@ -114,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++) @@ -160,6 +159,15 @@ public void UnsuccessfulCompilation() 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))); + } } } }