diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexNode.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexNode.cs
index 10ec814bea2649..28d47963777cfd 100644
--- a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexNode.cs
+++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexNode.cs
@@ -2021,6 +2021,43 @@ private RegexNode ReduceLookaround()
Debug.Assert(Kind is RegexNodeKind.PositiveLookaround or RegexNodeKind.NegativeLookaround);
Debug.Assert(ChildCount() == 1);
+ // Captures inside of negative lookarounds are undone after the lookaround. Thus, if there's nothing
+ // inside of the negative lookaround that needs that capture group (namely a backreference), we can
+ // remove the capture.
+ if (Kind is RegexNodeKind.NegativeLookaround && ContainsBackreference(Child(0)) is false)
+ {
+ if (RemoveCaptures(this, 0))
+ {
+ // If we removed captures, we may have changed the structure of the tree in a way that exposed more
+ // optimization possibility, so re-reduce the children.
+ ReplaceChild(0, Child(0));
+ }
+
+ static bool RemoveCaptures(RegexNode parent, int nodeIndex)
+ {
+ RegexNode node = parent.Child(nodeIndex);
+
+ if (node.Kind is RegexNodeKind.Capture)
+ {
+ parent.ReplaceChild(nodeIndex, node.Child(0));
+ RemoveCaptures(parent, nodeIndex);
+ return true;
+ }
+
+ bool changesMade = false;
+ if (StackHelper.TryEnsureSufficientExecutionStack())
+ {
+ int childCount = node.ChildCount();
+ for (int i = 0; i < childCount; i++)
+ {
+ changesMade |= RemoveCaptures(node, i);
+ }
+ }
+
+ return changesMade;
+ }
+ }
+
// A lookaround is a zero-width atomic assertion.
// As it's atomic, nothing will backtrack into it, and we can
// eliminate any ending backtracking from it.
@@ -2043,6 +2080,32 @@ private RegexNode ReduceLookaround()
return this;
}
+ /// Gets whether the node contains a backreference anywhere in its tree.
+ private static bool? ContainsBackreference(RegexNode node)
+ {
+ if (node.Kind is RegexNodeKind.Backreference or RegexNodeKind.BackreferenceConditional)
+ {
+ return true;
+ }
+
+ if (!StackHelper.TryEnsureSufficientExecutionStack())
+ {
+ // If we can't recur further, just stop optimizing.
+ return null;
+ }
+
+ int childCount = node.ChildCount();
+ for (int i = 0; i < childCount; i++)
+ {
+ if (ContainsBackreference(node.Child(i)) is true)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
/// Optimizations for backreference conditionals.
private RegexNode ReduceBackreferenceConditional()
{
diff --git a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/RegexReductionTests.cs b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/RegexReductionTests.cs
index 31967fb8d965b8..ef4c64eb062022 100644
--- a/src/libraries/System.Text.RegularExpressions/tests/UnitTests/RegexReductionTests.cs
+++ b/src/libraries/System.Text.RegularExpressions/tests/UnitTests/RegexReductionTests.cs
@@ -256,6 +256,10 @@ public class RegexReductionTests
[InlineData("(?>(?>(?>(?>))))", "")]
[InlineData("(?>(?>(?>(?>(?!)))))", "(?!)")]
[InlineData("(?=(?>))", "")]
+ // Lookaround reduction
+ [InlineData("(?!(abc))", "(?!abc)")]
+ [InlineData("(?!a(b*)c)", "(?!ab*c)")]
+ [InlineData("(?!a((((b))))c)", "(?!abc)")]
// Alternation reduction
[InlineData("a|b", "[ab]")]
[InlineData("a|b|c|d|e|g|h|z", "[a-eghz]")]
@@ -518,6 +522,10 @@ public void PatternsReduceIdentically(string actual, string expected)
[InlineData("(abc?)*?d", "(?>(ab(?>c?))*)d")]
[InlineData("(aba)+d", "(?>(aba)+)d")]
[InlineData("(abc*)*d", "(?>(ab(?>c*))*)d")]
+ // Lookaround reduction
+ [InlineData("(?=(abc))", "(?=abc)")]
+ [InlineData("(?=a(b*)c)", "(?=ab*c)")]
+ [InlineData("(?=a((((b))))c)", "(?=abc)")]
// Loops inside alternation constructs
[InlineData("(abc*|def)chi", "(ab(?>c*)|def)chi")]
[InlineData("(abc|def*)fhi", "(abc|de(?>f*))fhi")]