diff --git a/src/Parlot/Fluent/ParseContext.cs b/src/Parlot/Fluent/ParseContext.cs index 8d13955..bf9870b 100644 --- a/src/Parlot/Fluent/ParseContext.cs +++ b/src/Parlot/Fluent/ParseContext.cs @@ -24,7 +24,7 @@ public class ParseContext /// When true, loop detection is disabled. This may be needed when the ParseContext itself is mutated /// during loops and can change the end result of parsing at the same location. /// - public bool DisableLoopDetection { get; set; } + public bool DisableLoopDetection { get; } /// /// Whether new lines are treated as normal chars or white spaces. Default is false. @@ -33,7 +33,7 @@ public class ParseContext /// When false, new lines will be skipped like any other white space. /// Otherwise new lines need to be read explicitly by a rule. /// - public bool UseNewLines { get; private set; } + public bool UseNewLines { get; } /// /// The scanner used for the parsing session. @@ -43,25 +43,33 @@ public class ParseContext /// /// Tracks parser-position pairs to detect infinite recursion at the same position. /// - private readonly HashSet _activeParserPositions = new(); + private readonly HashSet _activeParserPositions; /// /// The cancellation token used to stop the parsing operation. /// public readonly CancellationToken CancellationToken; - public ParseContext(Scanner scanner, bool useNewLines = false) + // TODO: For backward compatibility only, remove in future versions + public ParseContext(Scanner scanner, bool useNewLines) + : this(scanner, useNewLines, false, CancellationToken.None) + { + } + + // TODO: For backward compatibility only, remove in future versions + public ParseContext(Scanner scanner, CancellationToken cancellationToken) + : this(scanner, false, false, cancellationToken) { - Scanner = scanner ?? throw new ArgumentNullException(nameof(scanner)); - UseNewLines = useNewLines; - CancellationToken = CancellationToken.None; } - public ParseContext(Scanner scanner, CancellationToken cancellationToken, bool useNewLines = false) + public ParseContext(Scanner scanner, bool useNewLines = false, bool disableLoopDetection = false, CancellationToken cancellationToken = default) { Scanner = scanner ?? throw new ArgumentNullException(nameof(scanner)); UseNewLines = useNewLines; CancellationToken = cancellationToken; + DisableLoopDetection = disableLoopDetection; + + _activeParserPositions = !disableLoopDetection ? new HashSet(ParserPositionComparer.Instance) : null!; } /// @@ -169,4 +177,24 @@ public void PopParserAtPosition(object parser, int position) /// Represents a parser instance at a specific position for cycle detection. /// private readonly record struct ParserPosition(object Parser, int Position); + + /// + /// Uses reference equality for parsers to avoid calling user GetHashCode overrides. + /// + private sealed class ParserPositionComparer : IEqualityComparer + { + public static readonly ParserPositionComparer Instance = new(); + + public bool Equals(ParserPosition x, ParserPosition y) => ReferenceEquals(x.Parser, y.Parser) && x.Position == y.Position; + + public int GetHashCode(ParserPosition obj) + { + unchecked + { + var hash = RuntimeHelpers.GetHashCode(obj.Parser); + hash = (hash * 397) ^ obj.Position; + return hash; + } + } + } } diff --git a/test/Parlot.Tests/FluentTests.cs b/test/Parlot.Tests/FluentTests.cs index 599d4fc..04b1976 100644 --- a/test/Parlot.Tests/FluentTests.cs +++ b/test/Parlot.Tests/FluentTests.cs @@ -1636,7 +1636,7 @@ public void DisableLoopDetectionShouldAllowInfiniteRecursion() loop.Parser = loop; // Test with loop detection enabled (default) - var contextWithDetection = new ParseContext(new Scanner("test")) { DisableLoopDetection = false }; + var contextWithDetection = new ParseContext(new Scanner("test"), disableLoopDetection: false); Assert.False(loop.TryParse(contextWithDetection, out var _, out var _)); // We can't safely test the DisableLoopDetection = true case to completion without stack overflow,