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,