From bcb6ce4c98b98db68a7752f4f9c86b2819bcc977 Mon Sep 17 00:00:00 2001 From: Andrii Kurdiumov Date: Sat, 27 Sep 2025 23:04:03 +0500 Subject: [PATCH] Use custom ISet which better perform for small number of elements, which is the usual case. --- .../Libraries/Parser/ParseErrorElement.cs | 16 +- .../Parser/ParseErrorElementDictionary.cs | 139 ++++++++++++--- .../Parser/ParseErrorExpectationSet.cs | 158 ++++++++++++++++++ .../Tests/Parser.Tests/ParseErrorTests.cs | 21 +++ 4 files changed, 306 insertions(+), 28 deletions(-) create mode 100644 Sources/SynKit/Libraries/Parser/ParseErrorExpectationSet.cs diff --git a/Sources/SynKit/Libraries/Parser/ParseErrorElement.cs b/Sources/SynKit/Libraries/Parser/ParseErrorElement.cs index c33f1079..d0005f1c 100644 --- a/Sources/SynKit/Libraries/Parser/ParseErrorElement.cs +++ b/Sources/SynKit/Libraries/Parser/ParseErrorElement.cs @@ -3,6 +3,7 @@ // Source repository: https://github.com/LanguageDev/Yoakke using System.Collections.Generic; +using System.Collections.Generic.Polyfill; namespace Yoakke.SynKit.Parser; @@ -11,10 +12,12 @@ namespace Yoakke.SynKit.Parser; /// public class ParseErrorElement { + private readonly ParseErrorExpectationSet expected; + /// /// The expected possible inputs. /// - public ISet Expected { get; } + public ISet Expected => this.expected; /// /// The context in which the error occurred. @@ -27,8 +30,9 @@ public class ParseErrorElement /// The expected input. /// The context in which the error occurred. public ParseErrorElement(object expected, string context) - : this(new HashSet { expected }, context) { + this.expected = new ParseErrorExpectationSet(expected); + this.Context = context; } /// @@ -38,7 +42,13 @@ public ParseErrorElement(object expected, string context) /// The context in which the error occurred. public ParseErrorElement(ISet expected, string context) { - this.Expected = expected; + this.expected = new ParseErrorExpectationSet(expected); this.Context = context; } + + internal ParseErrorElement CreateMergedElement(ParseErrorElement parseErrorElement) + { + var newExpected = parseErrorElement.expected.Merge(this.expected); + return new(newExpected, parseErrorElement.Context); + } } diff --git a/Sources/SynKit/Libraries/Parser/ParseErrorElementDictionary.cs b/Sources/SynKit/Libraries/Parser/ParseErrorElementDictionary.cs index 3514534a..d98fd1ad 100644 --- a/Sources/SynKit/Libraries/Parser/ParseErrorElementDictionary.cs +++ b/Sources/SynKit/Libraries/Parser/ParseErrorElementDictionary.cs @@ -5,7 +5,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Collections.Generic.Polyfill; using System.Linq; namespace Yoakke.SynKit.Parser; @@ -14,6 +13,8 @@ internal class ParseErrorElementDictionary : IReadOnlyDictionary? elements; private ParseErrorElementDictionary() @@ -31,25 +32,60 @@ public ParseErrorElementDictionary(string key, ParseErrorElement value) this.firstItem = value; } + public ParseErrorElementDictionary(string firstKey, ParseErrorElement firstValue, string secondKey, ParseErrorElement secondValue) + { + this.firstKey = firstKey; + this.firstItem = firstValue; + this.secondKey = secondKey; + this.secondItem = secondValue; + } + public ParseErrorElement this[string key] => this.firstKey is null ? this.elements is null ? throw new KeyNotFoundException($"The key {key} was not found in the dictionary") : this.elements[key] : this.firstKey == key ? this.firstItem! + : this.secondKey == key + ? this.secondItem! : throw new KeyNotFoundException($"The key {key} was not found in the dictionary"); - public IEnumerable Keys => this.firstKey is null ? this.elements!.Keys : new[] { this.firstKey }; + public IEnumerable Keys => + this.elements is not null + ? this.elements.Keys + : (this.secondKey is null) ? [this.firstKey!] : [this.firstKey!, this.secondKey]; - public IEnumerable Values => this.firstKey is null ? this.elements!.Values : new[] { this.firstItem! }; + public IEnumerable Values => + this.elements is not null + ? this.elements.Values + : (this.secondKey is null) ? [this.firstItem!] : [this.firstItem!, this.secondItem!]; - public int Count => this.firstKey is null ? this.elements is null ? 0 : this.elements.Count : 1; + public int Count => this.elements is null + ? this.firstKey is null ? 0 + : this.secondKey is null ? 1 : 2 + : this.elements.Count; + + public bool ContainsKey(string key) + { + if (this.elements is not null) + { + return this.elements.ContainsKey(key); + } + else if (this.firstKey is null) + { + return false; + } + else + { + return this.firstKey == key || (this.secondKey is null ? false : this.firstKey == key); + } + } - public bool ContainsKey(string key) => this.firstKey is null ? this.elements is null ? false : this.elements.ContainsKey(key) : this.firstKey == key; public IEnumerator> GetEnumerator() => this.elements is null - ? new Enumerator(this.firstKey!, this.firstItem!) - : this.elements.GetEnumerator(); + ? (secondKey is null) + ? new Enumerator(this.firstKey!, this.firstItem!) : new TwoElementEnumerator(this.firstKey!, this.firstItem!, this.secondKey, this.secondItem) + : this.elements.GetEnumerator(); public bool TryGetValue(string key, out ParseErrorElement value) { if (this.elements is null) @@ -60,6 +96,12 @@ public bool TryGetValue(string key, out ParseErrorElement value) return true; } + if (this.secondKey == key) + { + value = secondItem; + return true; + } + value = default; return false; } @@ -73,30 +115,31 @@ public ParseErrorElementDictionary Merge(ParseErrorElementDictionary other) { if (this.elements is null) { - if (other.elements is null) + if (other.elements is not null) { - if (this.firstKey == other.firstKey) - { - var newExpected = other.firstItem!.Expected.ToHashSet(); - newExpected.UnionWith(this.firstItem!.Expected); - return new(other.firstKey!, new (newExpected, this.firstItem.Context)); - } - else - { - return new(new Dictionary - { - { this.firstKey!, this.firstItem! }, - { other.firstKey!, other.firstItem! }, - }); - } + return new(new(other.elements)); + } + + if (this.firstKey == other.firstKey) + { + return new(other.firstKey!, other.firstItem!.CreateMergedElement(this.firstItem!)); } else { - return new(new (other.elements)); + if (this.secondKey is null) + { + return new(this.firstKey!, this.firstItem!, other.firstKey!, other.firstItem!); + } + return new(new Dictionary + { + { this.firstKey!, this.firstItem! }, + { this.secondKey!, this.secondItem! }, + { other.firstKey!, other.firstItem! }, + }); } } - var elements = this.elements.Values.ToDictionary(e => e.Context, e => new ParseErrorElement(e.Expected.ToHashSet(), e.Context)); + var elements = this.elements.Values.ToDictionary(e => e.Context, e => new ParseErrorElement(e.Expected, e.Context)); foreach (var element in other.Values) { if (elements.TryGetValue(element.Context, out var part)) @@ -105,7 +148,7 @@ public ParseErrorElementDictionary Merge(ParseErrorElementDictionary other) } else { - part = new(element.Expected.ToHashSet(), element.Context); + part = new(element.Expected, element.Context); elements.Add(element.Context, part); } } @@ -156,4 +199,50 @@ public void Reset() this.valid = false; } } + + private struct TwoElementEnumerator : IEnumerator> + { + private int consumed; + private KeyValuePair _firstPair; + private KeyValuePair _secondPair; + public TwoElementEnumerator(string firstKey, ParseErrorElement firstValue, string secondKey, ParseErrorElement secondValue) + { + this._firstPair = new KeyValuePair(firstKey, firstValue); + this._secondPair = new KeyValuePair(secondKey, secondValue); + } + + public KeyValuePair Current + { + get + { + if (consumed > 2) + { + throw new InvalidOperationException("The enumerator is not valid."); + } + + return consumed == 1 ? _firstPair : _secondPair; + } + } + + object IEnumerator.Current => this.Current; + + public void Dispose() { } + public bool MoveNext() + { + if (this.consumed >= 2) + { + this.consumed++; + return false; + } + else + { + this.consumed++; + return true; + } + } + public void Reset() + { + this.consumed = 0; + } + } } diff --git a/Sources/SynKit/Libraries/Parser/ParseErrorExpectationSet.cs b/Sources/SynKit/Libraries/Parser/ParseErrorExpectationSet.cs new file mode 100644 index 00000000..21ffa869 --- /dev/null +++ b/Sources/SynKit/Libraries/Parser/ParseErrorExpectationSet.cs @@ -0,0 +1,158 @@ +// Copyright (c) 2021-2022 Yoakke. +// Licensed under the Apache License, Version 2.0. +// Source repository: https://github.com/LanguageDev/Yoakke + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Generic.Polyfill; +using System.Linq; +using System.Net.Sockets; +using System.Text; + +namespace Yoakke.SynKit.Parser; + +internal class ParseErrorExpectationSet : ISet +{ + private object? firstItem; + private HashSet? elements; + + public ParseErrorExpectationSet(ISet expected) + { + if (expected.Count == 1) + { + this.firstItem = expected.First(); + } + else + { + this.elements = [.. expected]; + } + } + + public ParseErrorExpectationSet(object expected) + { + this.firstItem = expected; + } + + public int Count => this.elements is null ? this.firstItem is null ? 0 : 1 : this.elements.Count; + + public bool IsReadOnly => true; + + public bool Add(object item) + { + if (this.elements is not null) + { + return this.elements.Add(item); + } + + if (this.firstItem is null) + { + this.firstItem = item; + return true; + } + else + { + if (this.firstItem == item) return false; + this.elements = new() { this.firstItem, item }; + return true; + } + } + public void Clear() + { + this.firstItem = null; + this.elements = null; + } + public bool Contains(object item) + { + if (this.elements is not null) + { + return this.elements.Contains(item); + } + + return this.firstItem == item; + } + public void CopyTo(object[] array, int arrayIndex) => throw new NotImplementedException(); + public void ExceptWith(IEnumerable other) => throw new NotImplementedException(); + public IEnumerator GetEnumerator() => (this.elements is not null) ? this.elements.GetEnumerator() : new Enumerator(this.firstItem); + public void IntersectWith(IEnumerable other) => throw new NotImplementedException(); + public bool IsProperSubsetOf(IEnumerable other) => throw new NotImplementedException(); + public bool IsProperSupersetOf(IEnumerable other) => throw new NotImplementedException(); + public bool IsSubsetOf(IEnumerable other) => throw new NotImplementedException(); + public bool IsSupersetOf(IEnumerable other) => throw new NotImplementedException(); + public bool Overlaps(IEnumerable other) => throw new NotImplementedException(); + public bool Remove(object item) => throw new NotImplementedException(); + public bool SetEquals(IEnumerable other) => throw new NotImplementedException(); + public void SymmetricExceptWith(IEnumerable other) => throw new NotImplementedException(); + public void UnionWith(IEnumerable other) => throw new NotImplementedException(); + + internal ParseErrorExpectationSet Merge(ParseErrorExpectationSet other) + { + if (this.elements is not null) + { + var newExpected = this.ToHashSet(); + newExpected.UnionWith(other); + return new ParseErrorExpectationSet(newExpected); + } + + if (this.firstItem is null) + { + return other; + } + + if (this.firstItem == other.firstItem) + { + return this; + } + + { + var newExpected = this.ToHashSet(); + newExpected.UnionWith(other); + return new ParseErrorExpectationSet(newExpected); + } + } + void ICollection.Add(object item) => throw new NotImplementedException(); + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + private struct Enumerator : IEnumerator + { + private bool valid; + private readonly object? _current; + public Enumerator(object? value) + { + this._current = value; + } + + public object Current + { + get + { + if (!valid) + { + throw new InvalidOperationException("The enumerator is not valid."); + } + + return _current ?? throw new InvalidOperationException("The enumerator is not valid."); + } + } + + object IEnumerator.Current => this.Current; + + public void Dispose() { } + public bool MoveNext() + { + if (!this.valid) + { + this.valid = true; + return true; + } + else + { + this.valid = false; + return false; + } + } + public void Reset() + { + this.valid = false; + } + } +} diff --git a/Sources/SynKit/Tests/Parser.Tests/ParseErrorTests.cs b/Sources/SynKit/Tests/Parser.Tests/ParseErrorTests.cs index 0d3e6cad..75993fde 100644 --- a/Sources/SynKit/Tests/Parser.Tests/ParseErrorTests.cs +++ b/Sources/SynKit/Tests/Parser.Tests/ParseErrorTests.cs @@ -112,4 +112,25 @@ public void EnumerateTwoElements() Assert.Equal("other_expression", result[1].Value.Context); Assert.True(result[1].Value.Expected.Contains("^")); } + + [Fact] + public void EnumerateThreeElements() + { + var firstError = new ParseError("^", null, 12, "expression"); + var secondError = new ParseError("^", null, 12, "other_expression"); + var thirdError = new ParseError("^", null, 12, "third_expression"); + var mergedError = firstError | secondError | thirdError; + + var result = mergedError.Elements.ToList(); + + Assert.Equal("expression", result[0].Key); + Assert.Equal("expression", result[0].Value.Context); + Assert.True(result[0].Value.Expected.Contains("^")); + Assert.Equal("other_expression", result[1].Key); + Assert.Equal("other_expression", result[1].Value.Context); + Assert.True(result[1].Value.Expected.Contains("^")); + Assert.Equal("third_expression", result[2].Key); + Assert.Equal("third_expression", result[2].Value.Context); + Assert.True(result[2].Value.Expected.Contains("^")); + } }