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 8ef87cbe14a99d..3026d35bdf3338 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 @@ -3848,15 +3848,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. @@ -3902,299 +3893,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 633c0fdff99d78..88a4211c251872 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 1314122c953bd5..b7f48470256d02 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 (@"(?