From 991d78a4ce2a5d6cbda903175285716eb23c617a Mon Sep 17 00:00:00 2001 From: Stephen Toub Date: Mon, 14 Jul 2025 18:23:48 -0400 Subject: [PATCH] Fix handling of late-discovered atomic lazy loops in compiler / source generator Lazy loops can be made automatically atomic in some situations by the optimizer, in which case their handling significantly simplifies, because a lazy atomic loop just becomes a repeater for the min iteration count. Most viable lazy loops are caught by the optimizer, but some aren't yet are determined to be treatable as atomic at emit time. EmitLazy was handling such cases incorrectly, resulting in a missing branch target and compilation failing. This fixes that two fold: 1. The optimizer is improved, so the discovered tests cases that were triggering this case no longer do. 2. The distinction is eliminated from EmitLazy, as the case is rare and it would take a lot more code to optimize for that case. --- .../gen/RegexGenerator.Emitter.cs | 403 +++++++------- .../Text/RegularExpressions/RegexCompiler.cs | 505 +++++++++--------- .../Text/RegularExpressions/RegexNode.cs | 35 +- .../FunctionalTests/Regex.Match.Tests.cs | 12 + 4 files changed, 475 insertions(+), 480 deletions(-) diff --git a/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Emitter.cs b/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Emitter.cs index 931b7976199f31..078142726cd52b 100644 --- a/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Emitter.cs +++ b/src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Emitter.cs @@ -3719,15 +3719,6 @@ void EmitLazy(RegexNode node) return; } - // We should only be here if the lazy loop isn't atomic due to an ancestor, as the optimizer should - // in such a case have lowered the loop's upper bound to its lower bound, at which point it would - // have been handled by the above delegation to EmitLoop. However, if the optimizer missed doing so, - // this loop could still be considered atomic by ancestor by its parent nodes, in which case we want - // to make sure the code emitted here conforms (e.g. doesn't leave any state erroneously on the stack). - // So, we assert it's not atomic, but still handle that case. - bool isAtomic = rm.Analysis.IsAtomicByAncestor(node); - Debug.Assert(!isAtomic, "An atomic lazy should have had its upper bound lowered to its lower bound."); - // We might loop any number of times. In order to ensure this loop and subsequent code sees sliceStaticPos // the same regardless, we always need it to contain the same value, and the easiest such value is 0. // So, we transfer sliceStaticPos to pos, and ensure that any path out of here has sliceStaticPos as 0. @@ -3762,247 +3753,245 @@ void EmitLazy(RegexNode node) writer.WriteLine(); // Iteration body - MarkLabel(body, emitSemicolon: isAtomic); + MarkLabel(body, emitSemicolon: false); // In case iterations are backtracked through and unwound, we need to store the current position (so that // matching can resume from that location), the current crawl position if captures are possible (so that // we can uncapture back to that position), and both the starting position from the iteration we're leaving // and whether we've seen an empty iteration (if iterations may be empty). Since there can be multiple // iterations, this state needs to be stored on to the backtracking stack. - if (!isAtomic) + + int stackCookie = CreateStackCookie(); + int entriesPerIteration = + 1/*pos*/ + + (iterationMayBeEmpty ? 2/*startingPos+sawEmpty*/ : 0) + + (expressionHasCaptures ? 1/*Crawlpos*/ : 0) + + (stackCookie != 0 ? 1 : 0); + EmitStackPush(stackCookie, + expressionHasCaptures && iterationMayBeEmpty ? ["pos", startingPos!, sawEmpty!, "base.Crawlpos()"] : + iterationMayBeEmpty ? ["pos", startingPos!, sawEmpty!] : + expressionHasCaptures ? ["pos", "base.Crawlpos()"] : + ["pos"]); + + if (iterationMayBeEmpty) { - int stackCookie = CreateStackCookie(); - int entriesPerIteration = - 1/*pos*/ + - (iterationMayBeEmpty ? 2/*startingPos+sawEmpty*/ : 0) + - (expressionHasCaptures ? 1/*Crawlpos*/ : 0) + - (stackCookie != 0 ? 1 : 0); - EmitStackPush(stackCookie, - expressionHasCaptures && iterationMayBeEmpty ? ["pos", startingPos!, sawEmpty!, "base.Crawlpos()"] : - iterationMayBeEmpty ? ["pos", startingPos!, sawEmpty!] : - expressionHasCaptures ? ["pos", "base.Crawlpos()"] : - ["pos"]); + // We need to store the current pos so we can compare it against pos after the iteration, in order to + // determine whether the iteration was empty. + writer.WriteLine($"{startingPos} = pos;"); + } - if (iterationMayBeEmpty) - { - // We need to store the current pos so we can compare it against pos after the iteration, in order to - // determine whether the iteration was empty. - writer.WriteLine($"{startingPos} = pos;"); - } + // Proactively increase the number of iterations. We do this prior to the match rather than once + // we know it's successful, because we need to decrement it as part of a failed match when + // backtracking; it's thus simpler to just always decrement it as part of a failed match, even + // when initially greedily matching the loop, which then requires we increment it before trying. + writer.WriteLine($"{iterationCount}++;"); - // Proactively increase the number of iterations. We do this prior to the match rather than once - // we know it's successful, because we need to decrement it as part of a failed match when - // backtracking; it's thus simpler to just always decrement it as part of a failed match, even - // when initially greedily matching the loop, which then requires we increment it before trying. - writer.WriteLine($"{iterationCount}++;"); + // Last but not least, we need to set the doneLabel that a failed match of the body will jump to. + // Such an iteration match failure may or may not fail the whole operation, depending on whether + // we've already matched the minimum required iterations, so we need to jump to a location that + // will make that determination. + string iterationFailedLabel = ReserveName("LazyLoopIterationNoMatch"); + doneLabel = iterationFailedLabel; - // Last but not least, we need to set the doneLabel that a failed match of the body will jump to. - // Such an iteration match failure may or may not fail the whole operation, depending on whether - // we've already matched the minimum required iterations, so we need to jump to a location that - // will make that determination. - string iterationFailedLabel = ReserveName("LazyLoopIterationNoMatch"); - doneLabel = iterationFailedLabel; + // Finally, emit the child. + Debug.Assert(sliceStaticPos == 0); + writer.WriteLine(); + EmitNode(child); + writer.WriteLine(); + TransferSliceStaticPosToPos(); // ensure sliceStaticPos remains 0 + if (doneLabel == iterationFailedLabel) + { + doneLabel = originalDoneLabel; + } - // Finally, emit the child. - Debug.Assert(sliceStaticPos == 0); - writer.WriteLine(); - EmitNode(child); - writer.WriteLine(); - TransferSliceStaticPosToPos(); // ensure sliceStaticPos remains 0 - if (doneLabel == iterationFailedLabel) + // Loop condition. Continue iterating if we've not yet reached the minimum. We just successfully + // matched an iteration, so the only reason we'd need to forcefully loop around again is if the + // minimum were at least 2. + if (minIterations >= 2) + { + writer.WriteLine($"// The lazy loop requires a minimum of {minIterations} iterations. If that many haven't yet matched, loop now."); + using (EmitBlock(writer, $"if ({CountIsLessThan(iterationCount, minIterations)})")) { - doneLabel = originalDoneLabel; + Goto(body); } + } - // Loop condition. Continue iterating if we've not yet reached the minimum. We just successfully - // matched an iteration, so the only reason we'd need to forcefully loop around again is if the - // minimum were at least 2. - if (minIterations >= 2) + if (iterationMayBeEmpty) + { + // If the last iteration was empty, we need to prevent further iteration from this point + // unless we backtrack out of this iteration. + writer.WriteLine("// If the iteration successfully matched zero-length input, record that an empty iteration was seen."); + using (EmitBlock(writer, $"if (pos == {startingPos})")) { - writer.WriteLine($"// The lazy loop requires a minimum of {minIterations} iterations. If that many haven't yet matched, loop now."); - using (EmitBlock(writer, $"if ({CountIsLessThan(iterationCount, minIterations)})")) - { - Goto(body); - } + writer.WriteLine($"{sawEmpty} = 1; // true"); } + writer.WriteLine(); + } - if (iterationMayBeEmpty) + // We matched the next iteration. Jump to the subsequent code. + Goto(endLoop); + writer.WriteLine(); + + // Now handle what happens when an iteration fails (and since a lazy loop only executes an iteration + // when it's required to satisfy the loop by definition of being lazy, the loop is failing). We need + // to reset state to what it was before just that iteration started. That includes resetting pos and + // clearing out any captures from that iteration. + writer.WriteLine("// The lazy loop iteration failed to match."); + MarkLabel(iterationFailedLabel, emitSemicolon: false); + if (doneLabel != originalDoneLabel || !GotoWillExitMatch(originalDoneLabel)) // we don't need to back anything out if we're about to exit TryMatchAtCurrentPosition anyway. + { + // Fail this loop iteration, including popping state off the backtracking stack that was pushed + // on as part of the failing iteration. + writer.WriteLine($"{iterationCount}--;"); + if (expressionHasCaptures) { - // If the last iteration was empty, we need to prevent further iteration from this point - // unless we backtrack out of this iteration. - writer.WriteLine("// If the iteration successfully matched zero-length input, record that an empty iteration was seen."); - using (EmitBlock(writer, $"if (pos == {startingPos})")) - { - writer.WriteLine($"{sawEmpty} = 1; // true"); - } - writer.WriteLine(); + EmitUncaptureUntil(StackPop()); } + EmitStackPop(stackCookie, iterationMayBeEmpty ? + [sawEmpty!, startingPos!, "pos"] : + ["pos"]); + SliceInputSpan(); - // We matched the next iteration. Jump to the subsequent code. - Goto(endLoop); - writer.WriteLine(); - - // Now handle what happens when an iteration fails (and since a lazy loop only executes an iteration - // when it's required to satisfy the loop by definition of being lazy, the loop is failing). We need - // to reset state to what it was before just that iteration started. That includes resetting pos and - // clearing out any captures from that iteration. - writer.WriteLine("// The lazy loop iteration failed to match."); - MarkLabel(iterationFailedLabel, emitSemicolon: false); - if (doneLabel != originalDoneLabel || !GotoWillExitMatch(originalDoneLabel)) // we don't need to back anything out if we're about to exit TryMatchAtCurrentPosition anyway. + // If the loop's child doesn't backtrack, then this loop has failed. + // If the loop's child does backtrack, we need to backtrack back into the previous iteration if there was one. + if (doneLabel == originalDoneLabel) { - // Fail this loop iteration, including popping state off the backtracking stack that was pushed - // on as part of the failing iteration. - writer.WriteLine($"{iterationCount}--;"); - if (expressionHasCaptures) - { - EmitUncaptureUntil(StackPop()); - } - EmitStackPop(stackCookie, iterationMayBeEmpty ? - [sawEmpty!, startingPos!, "pos"] : - ["pos"]); - SliceInputSpan(); - - // If the loop's child doesn't backtrack, then this loop has failed. - // If the loop's child does backtrack, we need to backtrack back into the previous iteration if there was one. - if (doneLabel == originalDoneLabel) - { - // Since the only reason we'd end up revisiting previous iterations of the lazy loop is if the child had backtracking constructs - // we'd backtrack into, and the child doesn't, the whole loop is failed and done. If we successfully processed any iterations, - // we thus need to pop all of the state we pushed onto the stack for those iterations, as we're exiting out to the parent who - // will expect the stack to be cleared of any child state. - Debug.Assert(entriesPerIteration >= 1); - writer.WriteLine(entriesPerIteration > 1 ? - $"stackpos -= {iterationCount} * {entriesPerIteration};" : - $"stackpos -= {iterationCount};"); - } - else + // Since the only reason we'd end up revisiting previous iterations of the lazy loop is if the child had backtracking constructs + // we'd backtrack into, and the child doesn't, the whole loop is failed and done. If we successfully processed any iterations, + // we thus need to pop all of the state we pushed onto the stack for those iterations, as we're exiting out to the parent who + // will expect the stack to be cleared of any child state. + Debug.Assert(entriesPerIteration >= 1); + writer.WriteLine(entriesPerIteration > 1 ? + $"stackpos -= {iterationCount} * {entriesPerIteration};" : + $"stackpos -= {iterationCount};"); + } + else + { + // The child has backtracking constructs. If we have no successful iterations previously processed, just bail. + // If we do have successful iterations previously processed, however, we need to backtrack back into the last one. + using (EmitBlock(writer, $"if ({iterationCount} > 0)")) { - // The child has backtracking constructs. If we have no successful iterations previously processed, just bail. - // If we do have successful iterations previously processed, however, we need to backtrack back into the last one. - using (EmitBlock(writer, $"if ({iterationCount} > 0)")) + writer.WriteLine($"// The lazy loop matched at least one iteration; backtrack into the last one."); + if (iterationMayBeEmpty) { - writer.WriteLine($"// The lazy loop matched at least one iteration; backtrack into the last one."); - if (iterationMayBeEmpty) - { - // If we saw empty, it must have been in the most recent iteration, as we wouldn't have - // allowed additional iterations after one that was empty. Thus, we reset it back to - // false prior to backtracking / undoing that iteration. - writer.WriteLine($"{sawEmpty} = 0; // false"); - } - Goto(doneLabel); + // If we saw empty, it must have been in the most recent iteration, as we wouldn't have + // allowed additional iterations after one that was empty. Thus, we reset it back to + // false prior to backtracking / undoing that iteration. + writer.WriteLine($"{sawEmpty} = 0; // false"); } - writer.WriteLine(); + Goto(doneLabel); } + writer.WriteLine(); } - Goto(originalDoneLabel); - writer.WriteLine(); + } + Goto(originalDoneLabel); + writer.WriteLine(); - MarkLabel(endLoop, emitSemicolon: false); - - // If the lazy loop is not atomic, then subsequent code may backtrack back into this lazy loop, either - // causing it to add additional iterations, or backtracking into existing iterations and potentially - // unwinding them. We need to do a timeout check, and then determine whether to branch back to add more - // iterations (if we haven't hit the loop's maximum iteration count and haven't seen an empty iteration) - // or unwind by branching back to the last backtracking location. Either way, we need a dedicated - // backtracking section that a subsequent construct will see as its backtracking target. - - // We need to ensure that some state (e.g. iteration count) is persisted if we're backtracked to. - // We also need to push the current position, so that subsequent iterations pick up at the right - // point (and subsequent expressions are almost certain to have changed the current pos). However, - // if we're not inside of a loop, the other local's used for this construct are sufficient, as nothing - // else will overwrite them between now and when backtracking occurs. If, however, we are inside - // of another loop, then any number of iterations might have such state that needs to be stored, - // and thus it needs to be pushed on to the backtracking stack. - bool isInLoop = rm.Analysis.IsInLoop(node); - stackCookie = CreateStackCookie(); - EmitStackPush(stackCookie, - !isInLoop ? (expressionHasCaptures ? ["pos", "base.Crawlpos()"] : ["pos"]) : - iterationMayBeEmpty ? (expressionHasCaptures ? ["pos", iterationCount, startingPos!, sawEmpty!, "base.Crawlpos()"] : ["pos", iterationCount, startingPos!, sawEmpty!]) : - expressionHasCaptures ? ["pos", iterationCount, "base.Crawlpos()"] : - ["pos", iterationCount]); + MarkLabel(endLoop, emitSemicolon: false); - string skipBacktrack = ReserveName("LazyLoopSkipBacktrack"); - Goto(skipBacktrack); - writer.WriteLine(); + // If the lazy loop is not atomic, then subsequent code may backtrack back into this lazy loop, either + // causing it to add additional iterations, or backtracking into existing iterations and potentially + // unwinding them. We need to do a timeout check, and then determine whether to branch back to add more + // iterations (if we haven't hit the loop's maximum iteration count and haven't seen an empty iteration) + // or unwind by branching back to the last backtracking location. Either way, we need a dedicated + // backtracking section that a subsequent construct will see as its backtracking target. + + // We need to ensure that some state (e.g. iteration count) is persisted if we're backtracked to. + // We also need to push the current position, so that subsequent iterations pick up at the right + // point (and subsequent expressions are almost certain to have changed the current pos). However, + // if we're not inside of a loop, the other local's used for this construct are sufficient, as nothing + // else will overwrite them between now and when backtracking occurs. If, however, we are inside + // of another loop, then any number of iterations might have such state that needs to be stored, + // and thus it needs to be pushed on to the backtracking stack. + bool isInLoop = rm.Analysis.IsInLoop(node); + stackCookie = CreateStackCookie(); + EmitStackPush(stackCookie, + !isInLoop ? (expressionHasCaptures ? ["pos", "base.Crawlpos()"] : ["pos"]) : + iterationMayBeEmpty ? (expressionHasCaptures ? ["pos", iterationCount, startingPos!, sawEmpty!, "base.Crawlpos()"] : ["pos", iterationCount, startingPos!, sawEmpty!]) : + expressionHasCaptures ? ["pos", iterationCount, "base.Crawlpos()"] : + ["pos", iterationCount]); - // Emit a backtracking section that checks the timeout, restores the loop's state, and jumps to - // the appropriate label. - string backtrack = ReserveName($"LazyLoopBacktrack"); - MarkLabel(backtrack, emitSemicolon: false); + string skipBacktrack = ReserveName("LazyLoopSkipBacktrack"); + Goto(skipBacktrack); + writer.WriteLine(); - // We're backtracking. Check the timeout. - EmitTimeoutCheckIfNeeded(writer, rm); + // Emit a backtracking section that checks the timeout, restores the loop's state, and jumps to + // the appropriate label. + string backtrack = ReserveName($"LazyLoopBacktrack"); + MarkLabel(backtrack, emitSemicolon: false); - if (expressionHasCaptures) + // We're backtracking. Check the timeout. + EmitTimeoutCheckIfNeeded(writer, rm); + + if (expressionHasCaptures) + { + EmitUncaptureUntil(StackPop()); + } + EmitStackPop(stackCookie, + !isInLoop ? ["pos"] : + iterationMayBeEmpty ? [sawEmpty!, startingPos!, iterationCount, "pos"] : + [iterationCount, "pos"]); + SliceInputSpan(); + + // Determine where to branch, either back to the lazy loop body to add an additional iteration, + // or to the last backtracking label. + if (maxIterations != int.MaxValue || iterationMayBeEmpty) + { + FinishEmitBlock clause; + + writer.WriteLine(); + if (maxIterations == int.MaxValue) { - EmitUncaptureUntil(StackPop()); + // If the last iteration matched empty, backtrack. + writer.WriteLine("// If the last iteration matched empty, don't continue lazily iterating. Instead, backtrack."); + clause = EmitBlock(writer, $"if ({sawEmpty} != 0)"); } - EmitStackPop(stackCookie, - !isInLoop ? ["pos"] : - iterationMayBeEmpty ? [sawEmpty!, startingPos!, iterationCount, "pos"] : - [iterationCount, "pos"]); - SliceInputSpan(); - - // Determine where to branch, either back to the lazy loop body to add an additional iteration, - // or to the last backtracking label. - if (maxIterations != int.MaxValue || iterationMayBeEmpty) + else if (iterationMayBeEmpty) { - FinishEmitBlock clause; + // If the last iteration matched empty or if we've reached our upper bound, backtrack. + writer.WriteLine($"// If the upper bound {maxIterations} has already been reached, or if the last"); + writer.WriteLine($"// iteration matched empty, don't continue lazily iterating. Instead, backtrack."); + clause = EmitBlock(writer, $"if ({CountIsGreaterThanOrEqualTo(iterationCount, maxIterations)} || {sawEmpty} != 0)"); + } + else + { + // If we've reached our upper bound, backtrack. + writer.WriteLine($"// If the upper bound {maxIterations} has already been reached,"); + writer.WriteLine($"// don't continue lazily iterating. Instead, backtrack."); + clause = EmitBlock(writer, $"if ({CountIsGreaterThanOrEqualTo(iterationCount, maxIterations)})"); + } - writer.WriteLine(); - if (maxIterations == int.MaxValue) - { - // If the last iteration matched empty, backtrack. - writer.WriteLine("// If the last iteration matched empty, don't continue lazily iterating. Instead, backtrack."); - clause = EmitBlock(writer, $"if ({sawEmpty} != 0)"); - } - else if (iterationMayBeEmpty) - { - // If the last iteration matched empty or if we've reached our upper bound, backtrack. - writer.WriteLine($"// If the upper bound {maxIterations} has already been reached, or if the last"); - writer.WriteLine($"// iteration matched empty, don't continue lazily iterating. Instead, backtrack."); - clause = EmitBlock(writer, $"if ({CountIsGreaterThanOrEqualTo(iterationCount, maxIterations)} || {sawEmpty} != 0)"); - } - else + using (clause) + { + // We're backtracking, which could either be to something prior to the lazy loop or to something + // inside of the lazy loop. If it's to something inside of the lazy loop, then either the loop + // will eventually succeed or we'll eventually end up unwinding back through the iterations all + // the way back to the loop not matching at all, in which case the state we first pushed on at the + // beginning of the !isAtomic section will get popped off. But if here we're instead going to jump + // to something prior to the lazy loop, then we need to pop off that state here. + if (doneLabel == originalDoneLabel) { - // If we've reached our upper bound, backtrack. - writer.WriteLine($"// If the upper bound {maxIterations} has already been reached,"); - writer.WriteLine($"// don't continue lazily iterating. Instead, backtrack."); - clause = EmitBlock(writer, $"if ({CountIsGreaterThanOrEqualTo(iterationCount, maxIterations)})"); + EmitAdd(writer, "stackpos", -entriesPerIteration); } - using (clause) + if (iterationMayBeEmpty) { - // We're backtracking, which could either be to something prior to the lazy loop or to something - // inside of the lazy loop. If it's to something inside of the lazy loop, then either the loop - // will eventually succeed or we'll eventually end up unwinding back through the iterations all - // the way back to the loop not matching at all, in which case the state we first pushed on at the - // beginning of the !isAtomic section will get popped off. But if here we're instead going to jump - // to something prior to the lazy loop, then we need to pop off that state here. - if (doneLabel == originalDoneLabel) - { - EmitAdd(writer, "stackpos", -entriesPerIteration); - } - - if (iterationMayBeEmpty) - { - // If we saw empty, it must have been in the most recent iteration, as we wouldn't have - // allowed additional iterations after one that was empty. Thus, we reset it back to - // false prior to backtracking / undoing that iteration. - writer.WriteLine($"{sawEmpty} = 0; // false"); - } - - Goto(doneLabel); + // If we saw empty, it must have been in the most recent iteration, as we wouldn't have + // allowed additional iterations after one that was empty. Thus, we reset it back to + // false prior to backtracking / undoing that iteration. + writer.WriteLine($"{sawEmpty} = 0; // false"); } + + Goto(doneLabel); } + } - // Otherwise, try to match another iteration. - Goto(body); - writer.WriteLine(); + // Otherwise, try to match another iteration. + Goto(body); + writer.WriteLine(); - doneLabel = backtrack; - MarkLabel(skipBacktrack); - } + doneLabel = backtrack; + MarkLabel(skipBacktrack); } // Emits the code to handle a loop (repeater) with a fixed number of iterations. diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexCompiler.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexCompiler.cs index c1948b957f9390..ac92a4abe8125f 100644 --- a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexCompiler.cs +++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexCompiler.cs @@ -3850,15 +3850,6 @@ void EmitLazy(RegexNode node) return; } - // We should only be here if the lazy loop isn't atomic due to an ancestor, as the optimizer should - // in such a case have lowered the loop's upper bound to its lower bound, at which point it would - // have been handled by the above delegation to EmitLoop. However, if the optimizer missed doing so, - // this loop could still be considered atomic by ancestor by its parent nodes, in which case we want - // to make sure the code emitted here conforms (e.g. doesn't leave any state erroneously on the stack). - // So, we assert it's not atomic, but still handle that case. - bool isAtomic = analysis.IsAtomicByAncestor(node); - Debug.Assert(!isAtomic, "An atomic lazy should have had its upper bound lowered to its lower bound."); - // We might loop any number of times. In order to ensure this loop and subsequent code sees sliceStaticPos // the same regardless, we always need it to contain the same value, and the easiest such value is 0. // So, we transfer sliceStaticPos to pos, and ensure that any path out of here has sliceStaticPos as 0. @@ -3904,299 +3895,297 @@ void EmitLazy(RegexNode node) // we can uncapture back to that position), and both the starting position from the iteration we're leaving // and whether we've seen an empty iteration (if iterations may be empty). Since there can be multiple // iterations, this state needs to be stored on to the backtracking stack. - if (!isAtomic) + + // base.runstack[stackpos++] = pos; + // base.runstack[stackpos++] = startingPos; + // base.runstack[stackpos++] = sawEmpty; + // base.runstack[stackpos++] = base.Crawlpos(); + int entriesPerIteration = 1/*pos*/ + (iterationMayBeEmpty ? 2/*startingPos+sawEmpty*/ : 0) + (expressionHasCaptures ? 1/*Crawlpos*/ : 0); + EmitStackResizeIfNeeded(entriesPerIteration); + EmitStackPush(() => Ldloc(pos)); + if (iterationMayBeEmpty) { - // base.runstack[stackpos++] = pos; - // base.runstack[stackpos++] = startingPos; - // base.runstack[stackpos++] = sawEmpty; - // base.runstack[stackpos++] = base.Crawlpos(); - int entriesPerIteration = 1/*pos*/ + (iterationMayBeEmpty ? 2/*startingPos+sawEmpty*/ : 0) + (expressionHasCaptures ? 1/*Crawlpos*/ : 0); - EmitStackResizeIfNeeded(entriesPerIteration); - EmitStackPush(() => Ldloc(pos)); - if (iterationMayBeEmpty) - { - EmitStackPush(() => Ldloc(startingPos!)); - EmitStackPush(() => Ldloc(sawEmpty!)); - } - if (expressionHasCaptures) - { - EmitStackPush(() => { Ldthis(); Call(CrawlposMethod); }); - } + EmitStackPush(() => Ldloc(startingPos!)); + EmitStackPush(() => Ldloc(sawEmpty!)); + } + if (expressionHasCaptures) + { + EmitStackPush(() => { Ldthis(); Call(CrawlposMethod); }); + } - if (iterationMayBeEmpty) - { - // We need to store the current pos so we can compare it against pos after the iteration, in order to - // determine whether the iteration was empty. - // startingPos = pos; - Ldloc(pos); - Stloc(startingPos!); - } + if (iterationMayBeEmpty) + { + // We need to store the current pos so we can compare it against pos after the iteration, in order to + // determine whether the iteration was empty. + // startingPos = pos; + Ldloc(pos); + Stloc(startingPos!); + } - // Proactively increase the number of iterations. We do this prior to the match rather than once - // we know it's successful, because we need to decrement it as part of a failed match when - // backtracking; it's thus simpler to just always decrement it as part of a failed match, even - // when initially greedily matching the loop, which then requires we increment it before trying. - // iterationCount++; - Ldloc(iterationCount); - Ldc(1); - Add(); - Stloc(iterationCount); + // Proactively increase the number of iterations. We do this prior to the match rather than once + // we know it's successful, because we need to decrement it as part of a failed match when + // backtracking; it's thus simpler to just always decrement it as part of a failed match, even + // when initially greedily matching the loop, which then requires we increment it before trying. + // iterationCount++; + Ldloc(iterationCount); + Ldc(1); + Add(); + Stloc(iterationCount); - // Last but not least, we need to set the doneLabel that a failed match of the body will jump to. - // Such an iteration match failure may or may not fail the whole operation, depending on whether - // we've already matched the minimum required iterations, so we need to jump to a location that - // will make that determination. - Label iterationFailedLabel = DefineLabel(); - doneLabel = iterationFailedLabel; + // Last but not least, we need to set the doneLabel that a failed match of the body will jump to. + // Such an iteration match failure may or may not fail the whole operation, depending on whether + // we've already matched the minimum required iterations, so we need to jump to a location that + // will make that determination. + Label iterationFailedLabel = DefineLabel(); + doneLabel = iterationFailedLabel; - // Finally, emit the child. - Debug.Assert(sliceStaticPos == 0); - EmitNode(child); - TransferSliceStaticPosToPos(); // ensure sliceStaticPos remains 0 - if (doneLabel == iterationFailedLabel) - { - doneLabel = originalDoneLabel; - } + // Finally, emit the child. + Debug.Assert(sliceStaticPos == 0); + EmitNode(child); + TransferSliceStaticPosToPos(); // ensure sliceStaticPos remains 0 + if (doneLabel == iterationFailedLabel) + { + doneLabel = originalDoneLabel; + } - // Loop condition. Continue iterating if we've not yet reached the minimum. We just successfully - // matched an iteration, so the only reason we'd need to forcefully loop around again is if the - // minimum were at least 2. - if (minIterations >= 2) - { - // if (iterationCount < minIterations) goto body; - Ldloc(iterationCount); - Ldc(minIterations); - BltFar(body); - } + // Loop condition. Continue iterating if we've not yet reached the minimum. We just successfully + // matched an iteration, so the only reason we'd need to forcefully loop around again is if the + // minimum were at least 2. + if (minIterations >= 2) + { + // if (iterationCount < minIterations) goto body; + Ldloc(iterationCount); + Ldc(minIterations); + BltFar(body); + } - if (iterationMayBeEmpty) - { - // If the last iteration was empty, we need to prevent further iteration from this point - // unless we backtrack out of this iteration. - // if (pos == startingPos) sawEmpty = 1; // true - Label skipSawEmptySet = DefineLabel(); - Ldloc(pos); - Ldloc(startingPos!); - Bne(skipSawEmptySet); - Ldc(1); - Stloc(sawEmpty!); - MarkLabel(skipSawEmptySet); - } + if (iterationMayBeEmpty) + { + // If the last iteration was empty, we need to prevent further iteration from this point + // unless we backtrack out of this iteration. + // if (pos == startingPos) sawEmpty = 1; // true + Label skipSawEmptySet = DefineLabel(); + Ldloc(pos); + Ldloc(startingPos!); + Bne(skipSawEmptySet); + Ldc(1); + Stloc(sawEmpty!); + MarkLabel(skipSawEmptySet); + } - // We matched the next iteration. Jump to the subsequent code. - // goto endLoop; - BrFar(endLoop); + // We matched the next iteration. Jump to the subsequent code. + // goto endLoop; + BrFar(endLoop); - // Now handle what happens when an iteration fails (and since a lazy loop only executes an iteration - // when it's required to satisfy the loop by definition of being lazy, the loop is failing). We need - // to reset state to what it was before just that iteration started. That includes resetting pos and - // clearing out any captures from that iteration. - MarkLabel(iterationFailedLabel); + // Now handle what happens when an iteration fails (and since a lazy loop only executes an iteration + // when it's required to satisfy the loop by definition of being lazy, the loop is failing). We need + // to reset state to what it was before just that iteration started. That includes resetting pos and + // clearing out any captures from that iteration. + MarkLabel(iterationFailedLabel); - // Fail this loop iteration, including popping state off the backtracking stack that was pushed - // on as part of the failing iteration. + // Fail this loop iteration, including popping state off the backtracking stack that was pushed + // on as part of the failing iteration. - // iterationCount--; - Ldloc(iterationCount); - Ldc(1); - Sub(); - Stloc(iterationCount); + // iterationCount--; + Ldloc(iterationCount); + Ldc(1); + Sub(); + Stloc(iterationCount); - // poppedCrawlPos = base.runstack[--stackpos]; - // while (base.Crawlpos() > poppedCrawlPos) base.Uncapture(); - // sawEmpty = base.runstack[--stackpos]; - // startingPos = base.runstack[--stackpos]; - // pos = base.runstack[--stackpos]; - // slice = inputSpan.Slice(pos); - EmitUncaptureUntilPopped(); - if (iterationMayBeEmpty) - { - EmitStackPop(); - Stloc(sawEmpty!); - EmitStackPop(); - Stloc(startingPos!); - } + // poppedCrawlPos = base.runstack[--stackpos]; + // while (base.Crawlpos() > poppedCrawlPos) base.Uncapture(); + // sawEmpty = base.runstack[--stackpos]; + // startingPos = base.runstack[--stackpos]; + // pos = base.runstack[--stackpos]; + // slice = inputSpan.Slice(pos); + EmitUncaptureUntilPopped(); + if (iterationMayBeEmpty) + { EmitStackPop(); - Stloc(pos); - SliceInputSpan(); + Stloc(sawEmpty!); + EmitStackPop(); + Stloc(startingPos!); + } + EmitStackPop(); + Stloc(pos); + SliceInputSpan(); - // If the loop's child doesn't backtrack, then this loop has failed. - // If the loop's child does backtrack, we need to backtrack back into the previous iteration if there was one. - if (doneLabel == originalDoneLabel) - { - // Since the only reason we'd end up revisiting previous iterations of the lazy loop is if the child had backtracking constructs - // we'd backtrack into, and the child doesn't, the whole loop is failed and done. If we successfully processed any iterations, - // we thus need to pop all of the state we pushed onto the stack for those iterations, as we're exiting out to the parent who - // will expect the stack to be cleared of any child state. - - // stackpos -= iterationCount * entriesPerIteration; - Debug.Assert(entriesPerIteration >= 1); - Ldloc(stackpos); - Ldloc(iterationCount); - if (entriesPerIteration > 1) - { - Ldc(entriesPerIteration); - Mul(); - } - Sub(); - Stloc(stackpos); + // If the loop's child doesn't backtrack, then this loop has failed. + // If the loop's child does backtrack, we need to backtrack back into the previous iteration if there was one. + if (doneLabel == originalDoneLabel) + { + // Since the only reason we'd end up revisiting previous iterations of the lazy loop is if the child had backtracking constructs + // we'd backtrack into, and the child doesn't, the whole loop is failed and done. If we successfully processed any iterations, + // we thus need to pop all of the state we pushed onto the stack for those iterations, as we're exiting out to the parent who + // will expect the stack to be cleared of any child state. - // goto originalDoneLabel; - BrFar(originalDoneLabel); - } - else + // stackpos -= iterationCount * entriesPerIteration; + Debug.Assert(entriesPerIteration >= 1); + Ldloc(stackpos); + Ldloc(iterationCount); + if (entriesPerIteration > 1) { - // The child has backtracking constructs. If we have no successful iterations previously processed, just bail. - // If we do have successful iterations previously processed, however, we need to backtrack back into the last one. + Ldc(entriesPerIteration); + Mul(); + } + Sub(); + Stloc(stackpos); - // if (iterationCount == 0) goto originalDoneLabel; - Ldloc(iterationCount); - Ldc(0); - BeqFar(originalDoneLabel); + // goto originalDoneLabel; + BrFar(originalDoneLabel); + } + else + { + // The child has backtracking constructs. If we have no successful iterations previously processed, just bail. + // If we do have successful iterations previously processed, however, we need to backtrack back into the last one. - if (iterationMayBeEmpty) - { - // If we saw empty, it must have been in the most recent iteration, as we wouldn't have - // allowed additional iterations after one that was empty. Thus, we reset it back to - // false prior to backtracking / undoing that iteration. - Ldc(0); - Stloc(sawEmpty!); - } + // if (iterationCount == 0) goto originalDoneLabel; + Ldloc(iterationCount); + Ldc(0); + BeqFar(originalDoneLabel); - // goto doneLabel; - BrFar(doneLabel); + if (iterationMayBeEmpty) + { + // If we saw empty, it must have been in the most recent iteration, as we wouldn't have + // allowed additional iterations after one that was empty. Thus, we reset it back to + // false prior to backtracking / undoing that iteration. + Ldc(0); + Stloc(sawEmpty!); } - MarkLabel(endLoop); + // goto doneLabel; + BrFar(doneLabel); + } - // If the lazy loop is not atomic, then subsequent code may backtrack back into this lazy loop, either - // causing it to add additional iterations, or backtracking into existing iterations and potentially - // unwinding them. We need to do a timeout check, and then determine whether to branch back to add more - // iterations (if we haven't hit the loop's maximum iteration count and haven't seen an empty iteration) - // or unwind by branching back to the last backtracking location. Either way, we need a dedicated - // backtracking section that a subsequent construct will see as its backtracking target. - // We need to ensure that some state (e.g. iteration count) is persisted if we're backtracked to. - // If we're not inside of a loop, the local's used for this construct are sufficient, as nothing - // else will overwrite them between now and when backtracking occurs. If, however, we are inside - // of another loop, then any number of iterations might have such state that needs to be stored, - // and thus it needs to be pushed on to the backtracking stack. - // base.runstack[stackpos++] = pos; - // base.runstack[stackpos++] = iterationCount; - // base.runstack[stackpos++] = startingPos; - // base.runstack[stackpos++] = sawEmpty; - bool isInLoop = analysis.IsInLoop(node); - EmitStackResizeIfNeeded(1 + (isInLoop ? 1 + (iterationMayBeEmpty ? 2 : 0) : 0) + (expressionHasCaptures ? 1 : 0)); - EmitStackPush(() => Ldloc(pos)); - if (isInLoop) - { - EmitStackPush(() => Ldloc(iterationCount)); - if (iterationMayBeEmpty) - { - EmitStackPush(() => Ldloc(startingPos!)); - EmitStackPush(() => Ldloc(sawEmpty!)); - } - } - if (expressionHasCaptures) + MarkLabel(endLoop); + + // If the lazy loop is not atomic, then subsequent code may backtrack back into this lazy loop, either + // causing it to add additional iterations, or backtracking into existing iterations and potentially + // unwinding them. We need to do a timeout check, and then determine whether to branch back to add more + // iterations (if we haven't hit the loop's maximum iteration count and haven't seen an empty iteration) + // or unwind by branching back to the last backtracking location. Either way, we need a dedicated + // backtracking section that a subsequent construct will see as its backtracking target. + // We need to ensure that some state (e.g. iteration count) is persisted if we're backtracked to. + // If we're not inside of a loop, the local's used for this construct are sufficient, as nothing + // else will overwrite them between now and when backtracking occurs. If, however, we are inside + // of another loop, then any number of iterations might have such state that needs to be stored, + // and thus it needs to be pushed on to the backtracking stack. + // base.runstack[stackpos++] = pos; + // base.runstack[stackpos++] = iterationCount; + // base.runstack[stackpos++] = startingPos; + // base.runstack[stackpos++] = sawEmpty; + bool isInLoop = analysis.IsInLoop(node); + EmitStackResizeIfNeeded(1 + (isInLoop ? 1 + (iterationMayBeEmpty ? 2 : 0) : 0) + (expressionHasCaptures ? 1 : 0)); + EmitStackPush(() => Ldloc(pos)); + if (isInLoop) + { + EmitStackPush(() => Ldloc(iterationCount)); + if (iterationMayBeEmpty) { - EmitStackPush(() => { Ldthis(); Call(CrawlposMethod); }); + EmitStackPush(() => Ldloc(startingPos!)); + EmitStackPush(() => Ldloc(sawEmpty!)); } + } + if (expressionHasCaptures) + { + EmitStackPush(() => { Ldthis(); Call(CrawlposMethod); }); + } - Label skipBacktrack = DefineLabel(); - BrFar(skipBacktrack); + Label skipBacktrack = DefineLabel(); + BrFar(skipBacktrack); - // Emit a backtracking section that checks the timeout, restores the loop's state, and jumps to - // the appropriate label. - Label backtrack = DefineLabel(); - MarkLabel(backtrack); + // Emit a backtracking section that checks the timeout, restores the loop's state, and jumps to + // the appropriate label. + Label backtrack = DefineLabel(); + MarkLabel(backtrack); - // We're backtracking. Check the timeout. - EmitTimeoutCheckIfNeeded(); + // We're backtracking. Check the timeout. + EmitTimeoutCheckIfNeeded(); - // int poppedCrawlPos = base.runstack[--stackpos]; - // while (base.Crawlpos() > poppedCrawlPos) base.Uncapture(); - EmitUncaptureUntilPopped(); + // int poppedCrawlPos = base.runstack[--stackpos]; + // while (base.Crawlpos() > poppedCrawlPos) base.Uncapture(); + EmitUncaptureUntilPopped(); - if (isInLoop) + if (isInLoop) + { + // sawEmpty = base.runstack[--stackpos]; + // startingPos = base.runstack[--stackpos]; + // iterationCount = base.runstack[--stackpos]; + // pos = base.runstack[--stackpos]; + if (iterationMayBeEmpty) { - // sawEmpty = base.runstack[--stackpos]; - // startingPos = base.runstack[--stackpos]; - // iterationCount = base.runstack[--stackpos]; - // pos = base.runstack[--stackpos]; - if (iterationMayBeEmpty) - { - EmitStackPop(); - Stloc(sawEmpty!); - EmitStackPop(); - Stloc(startingPos!); - } EmitStackPop(); - Stloc(iterationCount); + Stloc(sawEmpty!); + EmitStackPop(); + Stloc(startingPos!); } EmitStackPop(); - Stloc(pos); - SliceInputSpan(); + Stloc(iterationCount); + } + EmitStackPop(); + Stloc(pos); + SliceInputSpan(); - // Determine where to branch, either back to the lazy loop body to add an additional iteration, - // or to the last backtracking label. + // Determine where to branch, either back to the lazy loop body to add an additional iteration, + // or to the last backtracking label. - Label jumpToDone = DefineLabel(); + Label jumpToDone = DefineLabel(); - if (iterationMayBeEmpty) - { - // if (sawEmpty != 0) - // { - // sawEmpty = 0; - // goto doneLabel; - // } - Label sawEmptyZero = DefineLabel(); - Ldloc(sawEmpty!); - Ldc(0); - Beq(sawEmptyZero); + if (iterationMayBeEmpty) + { + // if (sawEmpty != 0) + // { + // sawEmpty = 0; + // goto doneLabel; + // } + Label sawEmptyZero = DefineLabel(); + Ldloc(sawEmpty!); + Ldc(0); + Beq(sawEmptyZero); - // We saw empty, and it must have been in the most recent iteration, as we wouldn't have - // allowed additional iterations after one that was empty. Thus, we reset it back to - // false prior to backtracking / undoing that iteration. - Ldc(0); - Stloc(sawEmpty!); + // We saw empty, and it must have been in the most recent iteration, as we wouldn't have + // allowed additional iterations after one that was empty. Thus, we reset it back to + // false prior to backtracking / undoing that iteration. + Ldc(0); + Stloc(sawEmpty!); - Br(jumpToDone); - MarkLabel(sawEmptyZero); - } + Br(jumpToDone); + MarkLabel(sawEmptyZero); + } - if (maxIterations != int.MaxValue) - { - // if (iterationCount >= maxIterations) goto doneLabel; - Ldloc(iterationCount); - Ldc(maxIterations); - Bge(jumpToDone); - } + if (maxIterations != int.MaxValue) + { + // if (iterationCount >= maxIterations) goto doneLabel; + Ldloc(iterationCount); + Ldc(maxIterations); + Bge(jumpToDone); + } - // goto body; - BrFar(body); + // goto body; + BrFar(body); - MarkLabel(jumpToDone); + MarkLabel(jumpToDone); - // We're backtracking, which could either be to something prior to the lazy loop or to something - // inside of the lazy loop. If it's to something inside of the lazy loop, then either the loop - // will eventually succeed or we'll eventually end up unwinding back through the iterations all - // the way back to the loop not matching at all, in which case the state we first pushed on at the - // beginning of the !isAtomic section will get popped off. But if here we're instead going to jump - // to something prior to the lazy loop, then we need to pop off that state here. - if (doneLabel == originalDoneLabel) - { - // stackpos -= entriesPerIteration; - Ldloc(stackpos); - Ldc(entriesPerIteration); - Sub(); - Stloc(stackpos); - } + // We're backtracking, which could either be to something prior to the lazy loop or to something + // inside of the lazy loop. If it's to something inside of the lazy loop, then either the loop + // will eventually succeed or we'll eventually end up unwinding back through the iterations all + // the way back to the loop not matching at all, in which case the state we first pushed on at the + // beginning of the !isAtomic section will get popped off. But if here we're instead going to jump + // to something prior to the lazy loop, then we need to pop off that state here. + if (doneLabel == originalDoneLabel) + { + // stackpos -= entriesPerIteration; + Ldloc(stackpos); + Ldc(entriesPerIteration); + Sub(); + Stloc(stackpos); + } - // goto done; - BrFar(doneLabel); + // goto done; + BrFar(doneLabel); - doneLabel = backtrack; - MarkLabel(skipBacktrack); - } + doneLabel = backtrack; + MarkLabel(skipBacktrack); } // Emits the code to handle a loop (repeater) with a fixed number of iterations. 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 b81599c77ee787..6d3ece0a760ccb 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 @@ -336,7 +336,7 @@ internal RegexNode FinalOptimize() // Only apply optimization when LTR to avoid needing additional code for the much rarer RTL case. // Also only apply these optimizations when not using NonBacktracking, as these optimizations are // all about avoiding things that are impactful for the backtracking engines but nops for non-backtracking. - if ((Options & (RegexOptions.RightToLeft | RegexOptions.NonBacktracking)) == 0) + if ((rootNode.Options & (RegexOptions.RightToLeft | RegexOptions.NonBacktracking)) == 0) { // Optimization: eliminate backtracking for loops. // For any single-character loop (Oneloop, Notoneloop, Setloop), see if we can automatically convert @@ -411,10 +411,9 @@ internal RegexNode FinalOptimize() private void EliminateEndingBacktracking() { if (!StackHelper.TryEnsureSufficientExecutionStack() || - (Options & (RegexOptions.RightToLeft | RegexOptions.NonBacktracking)) != 0) + (Options & RegexOptions.NonBacktracking) != 0) { // If we can't recur further, just stop optimizing. - // We haven't done the work to validate this is correct for RTL. // And NonBacktracking doesn't support atomic groups and doesn't have backtracking to be eliminated. return; } @@ -423,6 +422,12 @@ private void EliminateEndingBacktracking() RegexNode node = this; while (true) { + // In general we don't care too much about RightToLeft performance, as it's rarely used as a top-level option, + // and thus haven't done the work to either implement or vet these optimizations for RTL. + // However, it's also used to implement lookbehinds, and so where possible we still want to optimize + // when we can do so easily. Most of these cases are appropriately trivial. + bool rtl = (node.Options & RegexOptions.RightToLeft) != 0; + switch (node.Kind) { // {One/Notone/Set}loops can be upgraded to {One/Notone/Set}loopatomic nodes, e.g. [abc]* => (?>[abc]*). @@ -448,7 +453,7 @@ private void EliminateEndingBacktracking() // an Atomic one if its grandparent is already Atomic. // e.g. [xyz](?:abc|def) => [xyz](?>abc|def) case RegexNodeKind.Capture: - case RegexNodeKind.Concatenate: + case RegexNodeKind.Concatenate when !rtl: RegexNode existingChild = node.Child(node.ChildCount() - 1); if ((existingChild.Kind is RegexNodeKind.Alternate or RegexNodeKind.BackreferenceConditional or RegexNodeKind.ExpressionConditional or RegexNodeKind.Loop or RegexNodeKind.Lazyloop) && (node.Parent is null || node.Parent.Kind != RegexNodeKind.Atomic)) // validate grandparent isn't atomic @@ -502,11 +507,14 @@ private void EliminateEndingBacktracking() continue; } - RegexNode? loopDescendent = node.FindLastExpressionInLoopForAutoAtomic(); - if (loopDescendent != null) + if (!rtl) { - node = loopDescendent; - continue; // loop around to process node + RegexNode? loopDescendent = node.FindLastExpressionInLoopForAutoAtomic(); + if (loopDescendent != null) + { + node = loopDescendent; + continue; // loop around to process node + } } } break; @@ -1807,12 +1815,6 @@ private void FindAndMakeLoopsAtomic() return; } - if ((Options & RegexOptions.RightToLeft) != 0) - { - // RTL is so rare, we don't need to spend additional time/code optimizing for it. - return; - } - // For all node types that have children, recur into each of those children. int childCount = ChildCount(); if (childCount != 0) @@ -1839,9 +1841,11 @@ private void FindAndMakeLoopsAtomic() static void ProcessNode(RegexNode node, RegexNode subsequent) { - if (!StackHelper.TryEnsureSufficientExecutionStack()) + if (!StackHelper.TryEnsureSufficientExecutionStack() || + (node.Options & RegexOptions.RightToLeft) != 0) { // If we can't recur further, just stop optimizing. + // And RTL is so rare, we don't need to spend additional time/code optimizing for it. return; } @@ -1936,6 +1940,7 @@ static void ProcessNode(RegexNode node, RegexNode subsequent) { RegexNode node = this; + Debug.Assert((node.Options & RegexOptions.RightToLeft) == 0, "Currently only implemented for left-to-right"); Debug.Assert(node.Kind is RegexNodeKind.Loop or RegexNodeKind.Lazyloop); // Start by looking at the loop's sole child. diff --git a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Match.Tests.cs b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Match.Tests.cs index df8858c9813833..4eb9f555fd2021 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Match.Tests.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Match.Tests.cs @@ -121,6 +121,18 @@ public static IEnumerable Match_MemberData() yield return (@"(\w)*?3(?<=33)$", "1233", RegexOptions.None, 0, 4, true, "1233"); yield return (@"(?=(\d))4\1", "44", RegexOptions.None, 0, 2, true, "44"); yield return (@"(?=(\d))4\1", "43", RegexOptions.None, 0, 2, false, ""); + yield return (@"(?<=()??)a", "a", RegexOptions.None, 0, 1, true, "a"); + yield return (@"(?<=()*?)a", "a", RegexOptions.None, 0, 1, true, "a"); + yield return (@"(?<=(){0,100}?)a", "a", RegexOptions.None, 0, 1, true, "a"); + yield return (@"(?