|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
namespace System.Text.RegularExpressions
{
/// <summary>
/// RegexCompiler translates a block of RegexCode to MSIL, and creates a subclass of the RegexRunner type.
/// </summary>
[RequiresDynamicCode("Compiling a RegEx requires dynamic code.")]
internal abstract class RegexCompiler
{
#pragma warning disable CS9264 // nullability of `field`: https://github.com/dotnet/csharplang/issues/8425
private static FieldInfo RuntextstartField => field ??= RegexRunnerField("runtextstart");
private static FieldInfo RuntextposField => field ??= RegexRunnerField("runtextpos");
private static FieldInfo RuntrackposField => field ??= RegexRunnerField("runtrackpos");
private static FieldInfo RunstackField => field ??= RegexRunnerField("runstack");
private static FieldInfo CultureField => field ??= typeof(CompiledRegexRunner).GetField("_culture", BindingFlags.Instance | BindingFlags.NonPublic)!;
private static FieldInfo CaseBehaviorField => field ??= typeof(CompiledRegexRunner).GetField("_caseBehavior", BindingFlags.Instance | BindingFlags.NonPublic)!;
private static FieldInfo SearchValuesArrayField => field ??= typeof(CompiledRegexRunner).GetField("_searchValues", BindingFlags.Instance | BindingFlags.NonPublic)!;
private static MethodInfo CaptureMethod => field ??= RegexRunnerMethod("Capture");
private static MethodInfo TransferCaptureMethod => field ??= RegexRunnerMethod("TransferCapture");
private static MethodInfo UncaptureMethod => field ??= RegexRunnerMethod("Uncapture");
private static MethodInfo IsMatchedMethod => field ??= RegexRunnerMethod("IsMatched");
private static MethodInfo MatchLengthMethod => field ??= RegexRunnerMethod("MatchLength");
private static MethodInfo MatchIndexMethod => field ??= RegexRunnerMethod("MatchIndex");
private static MethodInfo IsBoundaryMethod => field ??= typeof(RegexRunner).GetMethod("IsBoundary", BindingFlags.NonPublic | BindingFlags.Static, [typeof(ReadOnlySpan<char>), typeof(int)])!;
private static MethodInfo IsWordCharMethod => field ??= RegexRunnerMethod("IsWordChar");
private static MethodInfo IsECMABoundaryMethod => field ??= typeof(RegexRunner).GetMethod("IsECMABoundary", BindingFlags.NonPublic | BindingFlags.Static, [typeof(ReadOnlySpan<char>), typeof(int)])!;
private static MethodInfo CrawlposMethod => field ??= RegexRunnerMethod("Crawlpos");
private static MethodInfo CharInClassMethod => field ??= RegexRunnerMethod("CharInClass");
private static MethodInfo CheckTimeoutMethod => field ??= RegexRunnerMethod("CheckTimeout");
private static MethodInfo RegexCaseEquivalencesTryFindCaseEquivalencesForCharWithIBehaviorMethod => field ??= typeof(RegexCaseEquivalences).GetMethod("TryFindCaseEquivalencesForCharWithIBehavior", BindingFlags.Static | BindingFlags.Public)!;
private static MethodInfo CharIsDigitMethod => field ??= typeof(char).GetMethod("IsDigit", [typeof(char)])!;
private static MethodInfo CharIsWhiteSpaceMethod => field ??= typeof(char).GetMethod("IsWhiteSpace", [typeof(char)])!;
private static MethodInfo CharIsControlMethod => field ??= typeof(char).GetMethod("IsControl", [typeof(char)])!;
private static MethodInfo CharIsLetterMethod => field ??= typeof(char).GetMethod("IsLetter", [typeof(char)])!;
private static MethodInfo CharIsAsciiDigitMethod => field ??= typeof(char).GetMethod("IsAsciiDigit", [typeof(char)])!;
private static MethodInfo CharIsAsciiLetterMethod => field ??= typeof(char).GetMethod("IsAsciiLetter", [typeof(char)])!;
private static MethodInfo CharIsAsciiLetterLowerMethod => field ??= typeof(char).GetMethod("IsAsciiLetterLower", [typeof(char)])!;
private static MethodInfo CharIsAsciiLetterUpperMethod => field ??= typeof(char).GetMethod("IsAsciiLetterUpper", [typeof(char)])!;
private static MethodInfo CharIsAsciiLetterOrDigitMethod => field ??= typeof(char).GetMethod("IsAsciiLetterOrDigit", [typeof(char)])!;
private static MethodInfo CharIsAsciiHexDigitMethod => field ??= typeof(char).GetMethod("IsAsciiHexDigit", [typeof(char)])!;
private static MethodInfo CharIsAsciiHexDigitLowerMethod => field ??= typeof(char).GetMethod("IsAsciiHexDigitLower", [typeof(char)])!;
private static MethodInfo CharIsAsciiHexDigitUpperMethod => field ??= typeof(char).GetMethod("IsAsciiHexDigitUpper", [typeof(char)])!;
private static MethodInfo CharIsLetterOrDigitMethod => field ??= typeof(char).GetMethod("IsLetterOrDigit", [typeof(char)])!;
private static MethodInfo CharIsLowerMethod => field ??= typeof(char).GetMethod("IsLower", [typeof(char)])!;
private static MethodInfo CharIsUpperMethod => field ??= typeof(char).GetMethod("IsUpper", [typeof(char)])!;
private static MethodInfo CharIsNumberMethod => field ??= typeof(char).GetMethod("IsNumber", [typeof(char)])!;
private static MethodInfo CharIsPunctuationMethod => field ??= typeof(char).GetMethod("IsPunctuation", [typeof(char)])!;
private static MethodInfo CharIsSeparatorMethod => field ??= typeof(char).GetMethod("IsSeparator", [typeof(char)])!;
private static MethodInfo CharIsSymbolMethod => field ??= typeof(char).GetMethod("IsSymbol", [typeof(char)])!;
private static MethodInfo CharGetUnicodeInfoMethod => field ??= typeof(char).GetMethod("GetUnicodeCategory", [typeof(char)])!;
private static MethodInfo SpanGetItemMethod => field ??= typeof(ReadOnlySpan<char>).GetMethod("get_Item", [typeof(int)])!;
private static MethodInfo SpanGetLengthMethod => field ??= typeof(ReadOnlySpan<char>).GetMethod("get_Length")!;
private static MethodInfo SpanIndexOfCharMethod => field ??= typeof(MemoryExtensions).GetMethod("IndexOf", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), Type.MakeGenericMethodParameter(0)])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanIndexOfSpanMethod => field ??= typeof(MemoryExtensions).GetMethod("IndexOf", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0))])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanIndexOfSpanStringComparisonMethod => field ??= typeof(MemoryExtensions).GetMethod("IndexOf", [typeof(ReadOnlySpan<char>), typeof(ReadOnlySpan<char>), typeof(StringComparison)])!;
private static MethodInfo SpanIndexOfAnyCharCharMethod => field ??= typeof(MemoryExtensions).GetMethod("IndexOfAny", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(0)])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanIndexOfAnyCharCharCharMethod => field ??= typeof(MemoryExtensions).GetMethod("IndexOfAny", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(0)])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanIndexOfAnySpanMethod => field ??= typeof(MemoryExtensions).GetMethod("IndexOfAny", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0))])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanIndexOfAnySearchValuesMethod => field ??= typeof(MemoryExtensions).GetMethod("IndexOfAny", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), typeof(SearchValues<>).MakeGenericType(Type.MakeGenericMethodParameter(0))])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanIndexOfAnySearchValuesStringMethod => field ??= typeof(MemoryExtensions).GetMethod("IndexOfAny", [typeof(ReadOnlySpan<char>), typeof(SearchValues<string>)])!;
private static MethodInfo SpanIndexOfAnyExceptCharMethod => field ??= typeof(MemoryExtensions).GetMethod("IndexOfAnyExcept", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), Type.MakeGenericMethodParameter(0)])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanIndexOfAnyExceptCharCharMethod => field ??= typeof(MemoryExtensions).GetMethod("IndexOfAnyExcept", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(0)])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanIndexOfAnyExceptCharCharCharMethod => field ??= typeof(MemoryExtensions).GetMethod("IndexOfAnyExcept", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(0)])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanIndexOfAnyExceptSpanMethod => field ??= typeof(MemoryExtensions).GetMethod("IndexOfAnyExcept", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0))])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanIndexOfAnyExceptSearchValuesMethod => field ??= typeof(MemoryExtensions).GetMethod("IndexOfAnyExcept", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), typeof(SearchValues<>).MakeGenericType(Type.MakeGenericMethodParameter(0))])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanIndexOfAnyInRangeMethod => field ??= typeof(MemoryExtensions).GetMethod("IndexOfAnyInRange", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(0)])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanIndexOfAnyExceptInRangeMethod => field ??= typeof(MemoryExtensions).GetMethod("IndexOfAnyExceptInRange", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(0)])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanLastIndexOfCharMethod => field ??= typeof(MemoryExtensions).GetMethod("LastIndexOf", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), Type.MakeGenericMethodParameter(0)])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanLastIndexOfAnyCharCharMethod => field ??= typeof(MemoryExtensions).GetMethod("LastIndexOfAny", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(0)])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanLastIndexOfAnyCharCharCharMethod => field ??= typeof(MemoryExtensions).GetMethod("LastIndexOfAny", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(0)])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanLastIndexOfAnySpanMethod => field ??= typeof(MemoryExtensions).GetMethod("LastIndexOfAny", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0))])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanLastIndexOfAnySearchValuesMethod => field ??= typeof(MemoryExtensions).GetMethod("LastIndexOfAny", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), typeof(SearchValues<>).MakeGenericType(Type.MakeGenericMethodParameter(0))])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanLastIndexOfSpanMethod => field ??= typeof(MemoryExtensions).GetMethod("LastIndexOf", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0))])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanLastIndexOfAnyExceptCharMethod => field ??= typeof(MemoryExtensions).GetMethod("LastIndexOfAnyExcept", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), Type.MakeGenericMethodParameter(0)])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanLastIndexOfAnyExceptCharCharMethod => field ??= typeof(MemoryExtensions).GetMethod("LastIndexOfAnyExcept", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(0)])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanLastIndexOfAnyExceptCharCharCharMethod => field ??= typeof(MemoryExtensions).GetMethod("LastIndexOfAnyExcept", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(0)])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanLastIndexOfAnyExceptSpanMethod => field ??= typeof(MemoryExtensions).GetMethod("LastIndexOfAnyExcept", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0))])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanLastIndexOfAnyExceptSearchValuesMethod => field ??= typeof(MemoryExtensions).GetMethod("LastIndexOfAnyExcept", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), typeof(SearchValues<>).MakeGenericType(Type.MakeGenericMethodParameter(0))])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanLastIndexOfAnyInRangeMethod => field ??= typeof(MemoryExtensions).GetMethod("LastIndexOfAnyInRange", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(0)])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanLastIndexOfAnyExceptInRangeMethod => field ??= typeof(MemoryExtensions).GetMethod("LastIndexOfAnyExceptInRange", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), Type.MakeGenericMethodParameter(0), Type.MakeGenericMethodParameter(0)])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanSliceIntMethod => field ??= typeof(ReadOnlySpan<char>).GetMethod("Slice", [typeof(int)])!;
private static MethodInfo SpanSliceIntIntMethod => field ??= typeof(ReadOnlySpan<char>).GetMethod("Slice", [typeof(int), typeof(int)])!;
private static MethodInfo SpanStartsWithSpanMethod => field ??= typeof(MemoryExtensions).GetMethod("StartsWith", [typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0)), typeof(ReadOnlySpan<>).MakeGenericType(Type.MakeGenericMethodParameter(0))])!.MakeGenericMethod(typeof(char));
private static MethodInfo SpanStartsWithSpanComparisonMethod => field ??= typeof(MemoryExtensions).GetMethod("StartsWith", [typeof(ReadOnlySpan<char>), typeof(ReadOnlySpan<char>), typeof(StringComparison)])!;
private static MethodInfo StringAsSpanMethod => field ??= typeof(MemoryExtensions).GetMethod("AsSpan", [typeof(string)])!;
private static MethodInfo StringGetCharsMethod => field ??= typeof(string).GetMethod("get_Chars", [typeof(int)])!;
private static MethodInfo ArrayResizeMethod => field ??= typeof(Array).GetMethod("Resize")!.MakeGenericMethod(typeof(int));
private static MethodInfo MathMinIntIntMethod => field ??= typeof(Math).GetMethod("Min", [typeof(int), typeof(int)])!;
private static MethodInfo MemoryMarshalGetArrayDataReferenceSearchValuesMethod => field ??= typeof(MemoryMarshal).GetMethod("GetArrayDataReference", [Type.MakeGenericMethodParameter(0).MakeArrayType()])!.MakeGenericMethod(typeof(SearchValues<char>))!;
private static MethodInfo UnsafeAsMethod => field ??= typeof(Unsafe).GetMethod("As", [typeof(object)])!;
#pragma warning restore CS9264
// Note:
// Single-range helpers like IsAsciiLetterLower, IsAsciiLetterUpper, IsAsciiDigit, and IsBetween aren't used here, as the IL generated for those
// single-range checks is as cheap as the method call, and there's no readability issue as with the source generator.
/// <summary>The ILGenerator currently in use.</summary>
protected ILGenerator? _ilg;
/// <summary>The options for the expression.</summary>
protected RegexOptions _options;
/// <summary>The <see cref="RegexTree"/> written for the expression.</summary>
protected RegexTree? _regexTree;
/// <summary>Whether this expression has a non-infinite timeout.</summary>
protected bool _hasTimeout;
/// <summary><see cref="SearchValues{T}"/> instances used by the expression.</summary>
protected List<object>? _searchValues;
/// <summary>Pool of Int32 LocalBuilders.</summary>
private Stack<LocalBuilder>? _int32LocalsPool;
/// <summary>Pool of ReadOnlySpan of char locals.</summary>
private Stack<LocalBuilder>? _readOnlySpanCharLocalsPool;
private static FieldInfo RegexRunnerField(string fieldname) => typeof(RegexRunner).GetField(fieldname, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)!;
private static MethodInfo RegexRunnerMethod(string methname) => typeof(RegexRunner).GetMethod(methname, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)!;
/// <summary>
/// Entry point to dynamically compile a regular expression. The expression is compiled to
/// an in-memory assembly.
/// </summary>
internal static RegexRunnerFactory? Compile(string pattern, RegexTree regexTree, RegexOptions options, bool hasTimeout) =>
new RegexLWCGCompiler().FactoryInstanceFromCode(pattern, regexTree, options, hasTimeout);
/// <summary>A macro for _ilg.DefineLabel</summary>
private Label DefineLabel() => _ilg!.DefineLabel();
/// <summary>A macro for _ilg.MarkLabel</summary>
private void MarkLabel(Label l) => _ilg!.MarkLabel(l);
/// <summary>A macro for _ilg.Emit(Opcodes.Ldstr, str)</summary>
protected void Ldstr(string str) => _ilg!.Emit(OpCodes.Ldstr, str);
/// <summary>A macro for the various forms of Ldc.</summary>
protected void Ldc(int i) => _ilg!.Emit(OpCodes.Ldc_I4, i);
/// <summary>A macro for _ilg.Emit(OpCodes.Ldc_I8).</summary>
protected void LdcI8(long i) => _ilg!.Emit(OpCodes.Ldc_I8, i);
/// <summary>A macro for _ilg.Emit(OpCodes.Ret).</summary>
protected void Ret() => _ilg!.Emit(OpCodes.Ret);
/// <summary>A macro for _ilg.Emit(OpCodes.Dup).</summary>
protected void Dup() => _ilg!.Emit(OpCodes.Dup);
/// <summary>A macro for _ilg.Emit(OpCodes.Rem_Un).</summary>
private void RemUn() => _ilg!.Emit(OpCodes.Rem_Un);
/// <summary>A macro for _ilg.Emit(OpCodes.Ceq).</summary>
private void Ceq() => _ilg!.Emit(OpCodes.Ceq);
/// <summary>A macro for _ilg.Emit(OpCodes.Cgt_Un).</summary>
private void CgtUn() => _ilg!.Emit(OpCodes.Cgt_Un);
/// <summary>A macro for _ilg.Emit(OpCodes.Clt_Un).</summary>
private void CltUn() => _ilg!.Emit(OpCodes.Clt_Un);
/// <summary>A macro for _ilg.Emit(OpCodes.Pop).</summary>
private void Pop() => _ilg!.Emit(OpCodes.Pop);
/// <summary>A macro for _ilg.Emit(OpCodes.Add).</summary>
private void Add() => _ilg!.Emit(OpCodes.Add);
/// <summary>A macro for _ilg.Emit(OpCodes.Sub).</summary>
private void Sub() => _ilg!.Emit(OpCodes.Sub);
/// <summary>A macro for _ilg.Emit(OpCodes.Mul).</summary>
private void Mul() => _ilg!.Emit(OpCodes.Mul);
/// <summary>A macro for _ilg.Emit(OpCodes.And).</summary>
private void And() => _ilg!.Emit(OpCodes.And);
/// <summary>A macro for _ilg.Emit(OpCodes.Or).</summary>
private void Or() => _ilg!.Emit(OpCodes.Or);
/// <summary>A macro for _ilg.Emit(OpCodes.Shl).</summary>
private void Shl() => _ilg!.Emit(OpCodes.Shl);
/// <summary>A macro for _ilg.Emit(OpCodes.Shr).</summary>
private void Shr() => _ilg!.Emit(OpCodes.Shr);
/// <summary>A macro for _ilg.Emit(OpCodes.Ldloc).</summary>
/// <remarks>ILGenerator will switch to the optimal form based on the local's index.</remarks>
private void Ldloc(LocalBuilder lt) => _ilg!.Emit(OpCodes.Ldloc, lt);
/// <summary>A macro for _ilg.Emit(OpCodes.Ldloca).</summary>
/// <remarks>ILGenerator will switch to the optimal form based on the local's index.</remarks>
private void Ldloca(LocalBuilder lt) => _ilg!.Emit(OpCodes.Ldloca, lt);
/// <summary>A macro for _ilg.Emit(OpCodes.Ldind_U2).</summary>
private void LdindU2() => _ilg!.Emit(OpCodes.Ldind_U2);
/// <summary>A macro for _ilg.Emit(OpCodes.Ldind_I4).</summary>
private void LdindI4() => _ilg!.Emit(OpCodes.Ldind_I4);
/// <summary>A macro for _ilg.Emit(OpCodes.Ldind_I8).</summary>
private void LdindI8() => _ilg!.Emit(OpCodes.Ldind_I8);
/// <summary>A macro for _ilg.Emit(OpCodes.Unaligned).</summary>
private void Unaligned(byte alignment) => _ilg!.Emit(OpCodes.Unaligned, alignment);
/// <summary>A macro for _ilg.Emit(OpCodes.Stloc).</summary>
/// <remarks>ILGenerator will switch to the optimal form based on the local's index.</remarks>
private void Stloc(LocalBuilder lt) => _ilg!.Emit(OpCodes.Stloc, lt);
/// <summary>A macro for _ilg.Emit(OpCodes.Ldarg_0).</summary>
protected void Ldthis() => _ilg!.Emit(OpCodes.Ldarg_0);
/// <summary>A macro for _ilgEmit(OpCodes.Ldarg_1) </summary>
private void Ldarg_1() => _ilg!.Emit(OpCodes.Ldarg_1);
/// <summary>A macro for Ldthis(); Ldfld();</summary>
protected void Ldthisfld(FieldInfo ft)
{
Ldthis();
_ilg!.Emit(OpCodes.Ldfld, ft);
}
/// <summary>A macro for Ldthis(); Ldflda();</summary>
protected void Ldthisflda(FieldInfo ft)
{
Ldthis();
_ilg!.Emit(OpCodes.Ldflda, ft);
}
/// <summary>Fetches the address of argument in passed in <paramref name="position"/></summary>
/// <param name="position">The position of the argument which address needs to be fetched.</param>
private void Ldarga_s(int position) => _ilg!.Emit(OpCodes.Ldarga_S, position);
/// <summary>A macro for Ldthis(); Ldfld(); Stloc();</summary>
private void Mvfldloc(FieldInfo ft, LocalBuilder lt)
{
Ldthisfld(ft);
Stloc(lt);
}
/// <summary>A macro for _ilg.Emit(OpCodes.Stfld).</summary>
protected void Stfld(FieldInfo ft) => _ilg!.Emit(OpCodes.Stfld, ft);
/// <summary>A macro for _ilg.Emit(OpCodes.Callvirt, mt).</summary>
protected void Callvirt(MethodInfo mt) => _ilg!.Emit(OpCodes.Callvirt, mt);
/// <summary>A macro for _ilg.Emit(OpCodes.Call, mt).</summary>
protected void Call(MethodInfo mt) => _ilg!.Emit(OpCodes.Call, mt);
/// <summary>A macro for _ilg.Emit(OpCodes.Brfalse) (short jump).</summary>
private void Brfalse(Label l) => _ilg!.Emit(OpCodes.Brfalse_S, l);
/// <summary>A macro for _ilg.Emit(OpCodes.Brfalse) (long form).</summary>
private void BrfalseFar(Label l) => _ilg!.Emit(OpCodes.Brfalse, l);
/// <summary>A macro for _ilg.Emit(OpCodes.Brtrue) (long form).</summary>
private void BrtrueFar(Label l) => _ilg!.Emit(OpCodes.Brtrue, l);
/// <summary>A macro for _ilg.Emit(OpCodes.Br) (long form).</summary>
private void BrFar(Label l) => _ilg!.Emit(OpCodes.Br, l);
/// <summary>A macro for _ilg.Emit(OpCodes.Ble) (long form).</summary>
private void BleFar(Label l) => _ilg!.Emit(OpCodes.Ble, l);
/// <summary>A macro for _ilg.Emit(OpCodes.Blt) (long form).</summary>
private void BltFar(Label l) => _ilg!.Emit(OpCodes.Blt, l);
/// <summary>A macro for _ilg.Emit(OpCodes.Blt_Un) (long form).</summary>
private void BltUnFar(Label l) => _ilg!.Emit(OpCodes.Blt_Un, l);
/// <summary>A macro for _ilg.Emit(OpCodes.Bge) (long form).</summary>
private void BgeFar(Label l) => _ilg!.Emit(OpCodes.Bge, l);
/// <summary>A macro for _ilg.Emit(OpCodes.Bge_Un) (long form).</summary>
private void BgeUnFar(Label l) => _ilg!.Emit(OpCodes.Bge_Un, l);
/// <summary>A macro for _ilg.Emit(OpCodes.Bne) (long form).</summary>
private void BneFar(Label l) => _ilg!.Emit(OpCodes.Bne_Un, l);
/// <summary>A macro for _ilg.Emit(OpCodes.Beq) (long form).</summary>
private void BeqFar(Label l) => _ilg!.Emit(OpCodes.Beq, l);
/// <summary>A macro for _ilg.Emit(OpCodes.Brtrue_S) (short jump).</summary>
private void Brtrue(Label l) => _ilg!.Emit(OpCodes.Brtrue_S, l);
/// <summary>A macro for _ilg.Emit(OpCodes.Br_S) (short jump).</summary>
private void Br(Label l) => _ilg!.Emit(OpCodes.Br_S, l);
/// <summary>A macro for _ilg.Emit(OpCodes.Ble_S) (short jump).</summary>
private void Ble(Label l) => _ilg!.Emit(OpCodes.Ble_S, l);
/// <summary>A macro for _ilg.Emit(OpCodes.Blt_S) (short jump).</summary>
private void Blt(Label l) => _ilg!.Emit(OpCodes.Blt_S, l);
/// <summary>A macro for _ilg.Emit(OpCodes.Bge_S) (short jump).</summary>
private void Bge(Label l) => _ilg!.Emit(OpCodes.Bge_S, l);
/// <summary>A macro for _ilg.Emit(OpCodes.Bge_Un_S) (short jump).</summary>
private void BgeUn(Label l) => _ilg!.Emit(OpCodes.Bge_Un_S, l);
/// <summary>A macro for _ilg.Emit(OpCodes.Bgt_S) (short jump).</summary>
private void Bgt(Label l) => _ilg!.Emit(OpCodes.Bgt_S, l);
/// <summary>A macro for _ilg.Emit(OpCodes.Bne_S) (short jump).</summary>
private void Bne(Label l) => _ilg!.Emit(OpCodes.Bne_Un_S, l);
/// <summary>A macro for _ilg.Emit(OpCodes.Beq_S) (short jump).</summary>
private void Beq(Label l) => _ilg!.Emit(OpCodes.Beq_S, l);
/// <summary>A macro for the Ldlen instruction.</summary>
private void Ldlen() => _ilg!.Emit(OpCodes.Ldlen);
/// <summary>A macro for the Ldelem_I4 instruction.</summary>
private void LdelemI4() => _ilg!.Emit(OpCodes.Ldelem_I4);
/// <summary>A macro for the Stelem_I4 instruction.</summary>
private void StelemI4() => _ilg!.Emit(OpCodes.Stelem_I4);
private void Switch(Label[] table) => _ilg!.Emit(OpCodes.Switch, table);
/// <summary>Declares a local bool.</summary>
private LocalBuilder DeclareBool() => _ilg!.DeclareLocal(typeof(bool));
/// <summary>Declares a local int.</summary>
private LocalBuilder DeclareInt32() => _ilg!.DeclareLocal(typeof(int));
/// <summary>Declares a local CultureInfo.</summary>
private LocalBuilder? DeclareTextInfo() => _ilg!.DeclareLocal(typeof(TextInfo));
/// <summary>Declares a local string.</summary>
private LocalBuilder DeclareString() => _ilg!.DeclareLocal(typeof(string));
private LocalBuilder DeclareReadOnlySpanChar() => _ilg!.DeclareLocal(typeof(ReadOnlySpan<char>));
/// <summary>Rents an Int32 local variable slot from the pool of locals.</summary>
/// <remarks>
/// Care must be taken to Dispose of the returned <see cref="RentedLocalBuilder"/> when it's no longer needed,
/// and also not to jump into the middle of a block involving a rented local from outside of that block.
/// </remarks>
private RentedLocalBuilder RentInt32Local() => new RentedLocalBuilder(
_int32LocalsPool ??= new Stack<LocalBuilder>(),
_int32LocalsPool.TryPop(out LocalBuilder? iterationLocal) ? iterationLocal : DeclareInt32());
/// <summary>Rents a ReadOnlySpan(char) local variable slot from the pool of locals.</summary>
/// <remarks>
/// Care must be taken to Dispose of the returned <see cref="RentedLocalBuilder"/> when it's no longer needed,
/// and also not to jump into the middle of a block involving a rented local from outside of that block.
/// </remarks>
private RentedLocalBuilder RentReadOnlySpanCharLocal() => new RentedLocalBuilder(
_readOnlySpanCharLocalsPool ??= new Stack<LocalBuilder>(1), // capacity == 1 as we currently don't expect overlapping instances
_readOnlySpanCharLocalsPool.TryPop(out LocalBuilder? iterationLocal) ? iterationLocal : DeclareReadOnlySpanChar());
/// <summary>Returned a rented local to the pool.</summary>
private struct RentedLocalBuilder : IDisposable
{
private readonly Stack<LocalBuilder> _pool;
private readonly LocalBuilder _local;
internal RentedLocalBuilder(Stack<LocalBuilder> pool, LocalBuilder local)
{
_local = local;
_pool = pool;
}
public static implicit operator LocalBuilder(RentedLocalBuilder local) => local._local;
public void Dispose()
{
Debug.Assert(_pool != null);
Debug.Assert(_local != null);
Debug.Assert(!_pool.Contains(_local));
_pool.Push(_local);
this = default;
}
}
/// <summary>Generates the implementation for TryFindNextPossibleStartingPosition.</summary>
protected void EmitTryFindNextPossibleStartingPosition()
{
Debug.Assert(_regexTree != null);
_int32LocalsPool?.Clear();
_readOnlySpanCharLocalsPool?.Clear();
LocalBuilder inputSpan = DeclareReadOnlySpanChar();
LocalBuilder pos = DeclareInt32();
bool rtl = (_options & RegexOptions.RightToLeft) != 0;
// Load necessary locals
// int pos = base.runtextpos;
// ReadOnlySpan<char> inputSpan = dynamicMethodArg; // TODO: We can reference the arg directly rather than using another local.
Mvfldloc(RuntextposField, pos);
Ldarg_1();
Stloc(inputSpan);
// Generate length check. If the input isn't long enough to possibly match, fail quickly.
// It's rare for min required length to be 0, so we don't bother special-casing the check,
// especially since we want the "return false" code regardless. This differs from the source
// generator, where the return false is emitted at the end of the find method, and thus
// avoids the branch for the 0 case.
int minRequiredLength = _regexTree.FindOptimizations.MinRequiredLength;
Debug.Assert(minRequiredLength >= 0);
Label returnFalse = DefineLabel();
Label finishedLengthCheck = DefineLabel();
// if (pos > inputSpan.Length - minRequiredLength) // or pos < minRequiredLength for rtl
// {
// base.runtextpos = inputSpan.Length; // or 0 for rtl
// return false;
// }
Ldloc(pos);
if (!rtl)
{
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
if (minRequiredLength > 0)
{
Ldc(minRequiredLength);
Sub();
}
Ble(finishedLengthCheck);
}
else
{
Ldc(minRequiredLength);
Bge(finishedLengthCheck);
}
MarkLabel(returnFalse);
Ldthis();
if (!rtl)
{
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
}
else
{
Ldc(0);
}
Stfld(RuntextposField);
Ldc(0);
Ret();
MarkLabel(finishedLengthCheck);
// Emit any anchors.
if (EmitAnchors())
{
return;
}
// Either anchors weren't specified, or they don't completely root all matches to a specific location.
switch (_regexTree.FindOptimizations.FindMode)
{
case FindNextStartingPositionMode.LeadingString_LeftToRight:
case FindNextStartingPositionMode.LeadingString_OrdinalIgnoreCase_LeftToRight:
case FindNextStartingPositionMode.LeadingStrings_LeftToRight:
case FindNextStartingPositionMode.LeadingStrings_OrdinalIgnoreCase_LeftToRight:
case FindNextStartingPositionMode.FixedDistanceString_LeftToRight:
EmitIndexOfString_LeftToRight();
break;
case FindNextStartingPositionMode.LeadingString_RightToLeft:
EmitIndexOf_RightToLeft();
break;
case FindNextStartingPositionMode.LeadingSet_LeftToRight:
case FindNextStartingPositionMode.FixedDistanceSets_LeftToRight:
EmitFixedSet_LeftToRight();
break;
case FindNextStartingPositionMode.LeadingSet_RightToLeft:
EmitFixedSet_RightToLeft();
break;
case FindNextStartingPositionMode.LiteralAfterLoop_LeftToRight:
EmitLiteralAfterAtomicLoop();
break;
default:
Debug.Fail($"Unexpected mode: {_regexTree.FindOptimizations.FindMode}");
goto case FindNextStartingPositionMode.NoSearch;
case FindNextStartingPositionMode.NoSearch:
// return true;
Ldc(1);
Ret();
break;
}
// Emits any anchors. Returns true if the anchor roots any match to a specific location and thus no further
// searching is required; otherwise, false.
bool EmitAnchors()
{
Label label;
// Anchors that fully implement TryFindNextPossibleStartingPosition, with a check that leads to immediate success or failure determination.
switch (_regexTree.FindOptimizations.FindMode)
{
case FindNextStartingPositionMode.LeadingAnchor_LeftToRight_Beginning:
// if (pos != 0) goto returnFalse;
// return true;
Ldloc(pos);
Ldc(0);
Bne(returnFalse);
Ldc(1);
Ret();
return true;
case FindNextStartingPositionMode.LeadingAnchor_LeftToRight_Start:
case FindNextStartingPositionMode.LeadingAnchor_RightToLeft_Start:
// if (pos != base.runtextstart) goto returnFalse;
// return true;
Ldloc(pos);
Ldthisfld(RuntextstartField);
Bne(returnFalse);
Ldc(1);
Ret();
return true;
case FindNextStartingPositionMode.LeadingAnchor_LeftToRight_EndZ:
// if (pos < inputSpan.Length - 1) base.runtextpos = inputSpan.Length - 1;
// return true;
label = DefineLabel();
Ldloc(pos);
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
Ldc(1);
Sub();
Bge(label);
Ldthis();
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
Ldc(1);
Sub();
Stfld(RuntextposField);
MarkLabel(label);
Ldc(1);
Ret();
return true;
case FindNextStartingPositionMode.LeadingAnchor_LeftToRight_End:
// if (pos < inputSpan.Length) base.runtextpos = inputSpan.Length;
// return true;
label = DefineLabel();
Ldloc(pos);
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
Bge(label);
Ldthis();
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
Stfld(RuntextposField);
MarkLabel(label);
Ldc(1);
Ret();
return true;
case FindNextStartingPositionMode.LeadingAnchor_RightToLeft_Beginning:
// if (pos != 0) base.runtextpos = 0;
// return true;
label = DefineLabel();
Ldloc(pos);
Ldc(0);
Beq(label);
Ldthis();
Ldc(0);
Stfld(RuntextposField);
MarkLabel(label);
Ldc(1);
Ret();
return true;
case FindNextStartingPositionMode.LeadingAnchor_RightToLeft_EndZ:
// if (pos < inputSpan.Length - 1 || ((uint)pos < (uint)inputSpan.Length && inputSpan[pos] != '\n') goto returnFalse;
// return true;
label = DefineLabel();
Ldloc(pos);
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
Ldc(1);
Sub();
Blt(returnFalse);
Ldloc(pos);
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
BgeUn(label);
Ldloca(inputSpan);
Ldloc(pos);
Call(SpanGetItemMethod);
LdindU2();
Ldc('\n');
Bne(returnFalse);
MarkLabel(label);
Ldc(1);
Ret();
return true;
case FindNextStartingPositionMode.LeadingAnchor_RightToLeft_End:
// if (pos < inputSpan.Length) goto returnFalse;
// return true;
Ldloc(pos);
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
Blt(returnFalse);
Ldc(1);
Ret();
return true;
case FindNextStartingPositionMode.TrailingAnchor_FixedLength_LeftToRight_End:
case FindNextStartingPositionMode.TrailingAnchor_FixedLength_LeftToRight_EndZ:
// Jump to the end, minus the min required length, which in this case is actually the fixed length.
{
int extraNewlineBump = _regexTree.FindOptimizations.FindMode == FindNextStartingPositionMode.TrailingAnchor_FixedLength_LeftToRight_EndZ ? 1 : 0;
label = DefineLabel();
Ldloc(pos);
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
Ldc(_regexTree.FindOptimizations.MinRequiredLength + extraNewlineBump);
Sub();
Bge(label);
Ldthis();
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
Ldc(_regexTree.FindOptimizations.MinRequiredLength + extraNewlineBump);
Sub();
Stfld(RuntextposField);
MarkLabel(label);
Ldc(1);
Ret();
return true;
}
}
// Now handle anchors that boost the position but don't determine immediate success or failure.
if (!rtl) // we haven't done the work to validate these optimizations for RightToLeft
{
switch (_regexTree.FindOptimizations.LeadingAnchor)
{
case RegexNodeKind.Bol:
{
// Optimize the handling of a Beginning-Of-Line (BOL) anchor. BOL is special, in that unlike
// other anchors like Beginning, there are potentially multiple places a BOL can match. So unlike
// the other anchors, which all skip all subsequent processing if found, with BOL we just use it
// to boost our position to the next line, and then continue normally with any prefix or char class searches.
label = DefineLabel();
// if (pos > 0...
Ldloc(pos!);
Ldc(0);
Ble(label);
// ... && inputSpan[pos - 1] != '\n') { ... }
Ldloca(inputSpan);
Ldloc(pos);
Ldc(1);
Sub();
Call(SpanGetItemMethod);
LdindU2();
Ldc('\n');
Beq(label);
// int tmp = inputSpan.Slice(pos).IndexOf('\n');
Ldloca(inputSpan);
Ldloc(pos);
Call(SpanSliceIntMethod);
Ldc('\n');
Call(SpanIndexOfCharMethod);
using (RentedLocalBuilder newlinePos = RentInt32Local())
{
Stloc(newlinePos);
// if (newlinePos < 0 || newlinePos + pos + 1 > inputSpan.Length)
// {
// base.runtextpos = inputSpan.Length;
// return false;
// }
Ldloc(newlinePos);
Ldc(0);
Blt(returnFalse);
Ldloc(newlinePos);
Ldloc(pos);
Add();
Ldc(1);
Add();
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
Bgt(returnFalse);
// pos += newlinePos + 1;
Ldloc(pos);
Ldloc(newlinePos);
Add();
Ldc(1);
Add();
Stloc(pos);
// We've updated the position. Make sure there's still enough room in the input for a possible match.
// if (pos > inputSpan.Length - minRequiredLength) returnFalse;
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
if (minRequiredLength != 0)
{
Ldc(minRequiredLength);
Sub();
}
Ldloc(pos);
BltFar(returnFalse);
}
MarkLabel(label);
}
break;
}
switch (_regexTree.FindOptimizations.TrailingAnchor)
{
case RegexNodeKind.End or RegexNodeKind.EndZ when _regexTree.FindOptimizations.MaxPossibleLength is int maxLength:
// Jump to the end, minus the max allowed length.
{
int extraNewlineBump = _regexTree.FindOptimizations.FindMode == FindNextStartingPositionMode.TrailingAnchor_FixedLength_LeftToRight_EndZ ? 1 : 0;
label = DefineLabel();
Ldloc(pos);
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
Ldc(maxLength + extraNewlineBump);
Sub();
Bge(label);
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
Ldc(maxLength + extraNewlineBump);
Sub();
Stloc(pos);
MarkLabel(label);
break;
}
}
}
return false;
}
// Emits a case-sensitive left-to-right search for a substring or substrings.
void EmitIndexOfString_LeftToRight()
{
RegexFindOptimizations opts = _regexTree.FindOptimizations;
Debug.Assert(opts.FindMode is FindNextStartingPositionMode.LeadingString_LeftToRight or
FindNextStartingPositionMode.LeadingString_OrdinalIgnoreCase_LeftToRight or
FindNextStartingPositionMode.FixedDistanceString_LeftToRight or
FindNextStartingPositionMode.LeadingStrings_LeftToRight or
FindNextStartingPositionMode.LeadingStrings_OrdinalIgnoreCase_LeftToRight);
using RentedLocalBuilder i = RentInt32Local();
// int i = inputSpan.Slice(pos)...
Ldloca(inputSpan);
Ldloc(pos);
if (opts.FindMode is FindNextStartingPositionMode.FixedDistanceString_LeftToRight &&
opts.FixedDistanceLiteral is { Distance: > 0 } literal)
{
Ldc(literal.Distance);
Add();
}
Call(SpanSliceIntMethod);
// ...IndexOf(prefix);
if (opts.FindMode is FindNextStartingPositionMode.LeadingStrings_LeftToRight or FindNextStartingPositionMode.LeadingStrings_OrdinalIgnoreCase_LeftToRight)
{
LoadSearchValues(opts.LeadingPrefixes, opts.FindMode is FindNextStartingPositionMode.LeadingStrings_OrdinalIgnoreCase_LeftToRight ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
Call(SpanIndexOfAnySearchValuesStringMethod);
}
else
{
string literalString = opts.FindMode is FindNextStartingPositionMode.LeadingString_LeftToRight or FindNextStartingPositionMode.LeadingString_OrdinalIgnoreCase_LeftToRight ?
opts.LeadingPrefix :
opts.FixedDistanceLiteral.String!;
LoadSearchValues([literalString], opts.FindMode is FindNextStartingPositionMode.LeadingString_OrdinalIgnoreCase_LeftToRight ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
Call(SpanIndexOfAnySearchValuesStringMethod);
}
Stloc(i);
// if (i < 0) goto ReturnFalse;
Ldloc(i);
Ldc(0);
BltFar(returnFalse);
// base.runtextpos = pos + i;
// return true;
Ldthis();
Ldloc(pos);
Ldloc(i);
Add();
Stfld(RuntextposField);
Ldc(1);
Ret();
}
// Emits a case-sensitive right-to-left search for a substring.
void EmitIndexOf_RightToLeft()
{
string prefix = _regexTree.FindOptimizations.LeadingPrefix;
Debug.Assert(!string.IsNullOrEmpty(prefix));
// pos = inputSpan.Slice(0, pos).LastIndexOf(prefix);
Ldloca(inputSpan);
Ldc(0);
Ldloc(pos);
Call(SpanSliceIntIntMethod);
Ldstr(prefix);
Call(StringAsSpanMethod);
Call(SpanLastIndexOfSpanMethod);
Stloc(pos);
// if (pos < 0) goto ReturnFalse;
Ldloc(pos);
Ldc(0);
BltFar(returnFalse);
// base.runtextpos = pos + prefix.Length;
// return true;
Ldthis();
Ldloc(pos);
Ldc(prefix.Length);
Add();
Stfld(RuntextposField);
Ldc(1);
Ret();
}
// Emits a search for a set at a fixed position from the start of the pattern,
// and potentially other sets at other fixed positions in the pattern.
void EmitFixedSet_LeftToRight()
{
Debug.Assert(_regexTree.FindOptimizations.FixedDistanceSets is { Count: > 0 });
List<RegexFindOptimizations.FixedDistanceSet>? sets = _regexTree.FindOptimizations.FixedDistanceSets;
RegexFindOptimizations.FixedDistanceSet primarySet = sets![0];
const int MaxSets = 4;
int setsToUse = Math.Min(sets.Count, MaxSets);
using RentedLocalBuilder iLocal = RentInt32Local();
using RentedLocalBuilder textSpanLocal = RentReadOnlySpanCharLocal();
// ReadOnlySpan<char> span = inputSpan.Slice(pos);
Ldloca(inputSpan);
Ldloc(pos);
Call(SpanSliceIntMethod);
Stloc(textSpanLocal);
// Use IndexOf{Any} to accelerate the skip loop via vectorization to match the first prefix.
// But we avoid using it for the relatively common case of the starting set being '.', aka anything other than
// a newline, as it's very rare to have long, uninterrupted sequences of newlines. And we avoid using it
// for the case of the starting set being anything (e.g. '.' with SingleLine), as in that case it'll always match
// the first char.
int setIndex = 0;
bool canUseIndexOf =
primarySet.Set != RegexCharClass.NotNewLineClass &&
primarySet.Set != RegexCharClass.AnyClass;
bool needLoop = !canUseIndexOf || setsToUse > 1;
Label checkSpanLengthLabel = default;
Label charNotInClassLabel = default;
Label loopBody = default;
if (needLoop)
{
checkSpanLengthLabel = DefineLabel();
charNotInClassLabel = DefineLabel();
loopBody = DefineLabel();
// for (int i = 0;
Ldc(0);
Stloc(iLocal);
BrFar(checkSpanLengthLabel);
MarkLabel(loopBody);
}
if (canUseIndexOf)
{
setIndex = 1;
if (needLoop)
{
// slice.Slice(iLocal + primarySet.Distance);
Ldloca(textSpanLocal);
Ldloc(iLocal);
if (primarySet.Distance != 0)
{
Ldc(primarySet.Distance);
Add();
}
Call(SpanSliceIntMethod);
}
else if (primarySet.Distance != 0)
{
// slice.Slice(primarySet.Distance)
Ldloca(textSpanLocal);
Ldc(primarySet.Distance);
Call(SpanSliceIntMethod);
}
else
{
// slice
Ldloc(textSpanLocal);
}
if (primarySet.Chars is not null)
{
Debug.Assert(primarySet.Chars.Length > 0);
switch (primarySet.Chars.Length)
{
case 1:
// tmp = ...IndexOf(setChars[0]);
Ldc(primarySet.Chars[0]);
Call(primarySet.Negated ? SpanIndexOfAnyExceptCharMethod : SpanIndexOfCharMethod);
break;
case 2:
// tmp = ...IndexOfAny(setChars[0], setChars[1]);
Ldc(primarySet.Chars[0]);
Ldc(primarySet.Chars[1]);
Call(primarySet.Negated ? SpanIndexOfAnyExceptCharCharMethod : SpanIndexOfAnyCharCharMethod);
break;
case 3:
// tmp = ...IndexOfAny(setChars[0], setChars[1], setChars[2]});
Ldc(primarySet.Chars[0]);
Ldc(primarySet.Chars[1]);
Ldc(primarySet.Chars[2]);
Call(primarySet.Negated ? SpanIndexOfAnyExceptCharCharCharMethod : SpanIndexOfAnyCharCharCharMethod);
break;
default:
// tmp = ...IndexOfAny(setChars);
// tmp = ...IndexOfAny(s_searchValues);
EmitIndexOfAnyWithSearchValuesOrLiteral(primarySet.Chars, except: primarySet.Negated);
break;
}
}
else if (primarySet.Range is not null)
{
if (primarySet.Range.Value.LowInclusive == primarySet.Range.Value.HighInclusive)
{
// tmp = ...IndexOf{AnyExcept}(low);
Ldc(primarySet.Range.Value.LowInclusive);
Call(primarySet.Negated ? SpanIndexOfAnyExceptCharMethod : SpanIndexOfCharMethod);
}
else
{
// tmp = ...IndexOfAny{Except}InRange(low, high);
Ldc(primarySet.Range.Value.LowInclusive);
Ldc(primarySet.Range.Value.HighInclusive);
Call(primarySet.Negated ? SpanIndexOfAnyExceptInRangeMethod : SpanIndexOfAnyInRangeMethod);
}
}
else if (RegexCharClass.IsUnicodeCategoryOfSmallCharCount(primarySet.Set, out char[]? setChars, out bool negated, out _))
{
// We have a known set of small number of characters; we can use IndexOfAny{Except}(searchValues).
// tmp = ...IndexOfAny(s_searchValues);
LoadSearchValues(setChars);
Call(negated ? SpanIndexOfAnyExceptSearchValuesMethod : SpanIndexOfAnySearchValuesMethod);
}
else
{
// In order to optimize the search for ASCII characters, we use SearchValues to vectorize a search
// for those characters plus anything non-ASCII (if we find something non-ASCII, we'll fall back to
// a sequential walk). In order to do that search, we actually build up a set for all of the ASCII
// characters _not_ contained in the set, and then do a search for the inverse of that, which will be
// all of the target ASCII characters and all of non-ASCII.
using var asciiChars = new ValueListBuilder<char>(stackalloc char[128]);
for (int i = 0; i < 128; i++)
{
if (!RegexCharClass.CharInClass((char)i, primarySet.Set))
{
asciiChars.Append((char)i);
}
}
using (RentedLocalBuilder span = RentReadOnlySpanCharLocal())
using (RentedLocalBuilder i = RentInt32Local())
{
// ReadOnlySpan<char> span = inputSpan...;
Stloc(span);
// int i = span.
Ldloc(span);
if (asciiChars.Length == 128)
{
// IndexOfAnyExceptInRange('\0', '\u007f');
Ldc(0);
Ldc(127);
Call(SpanIndexOfAnyExceptInRangeMethod);
}
else
{
// IndexOfAnyExcept(searchValuesArray[...]);
LoadSearchValues(asciiChars.AsSpan().ToArray());
Call(SpanIndexOfAnyExceptSearchValuesMethod);
}
Stloc(i);
// if ((uint)i >= span.Length) goto doneSearch;
Label doneSearch = DefineLabel();
Ldloc(i);
Ldloca(span);
Call(SpanGetLengthMethod);
BgeUnFar(doneSearch);
// if (span[i] <= 0x7f) goto doneSearch;
Ldc(0x7f);
Ldloca(span);
Ldloc(i);
Call(SpanGetItemMethod);
LdindU2();
BgeUnFar(doneSearch);
Label loop = DefineLabel();
MarkLabel(loop);
// do { ...
// if (CharInClass(span[i])) goto doneSearch;
Ldloca(span);
Ldloc(i);
Call(SpanGetItemMethod);
LdindU2();
EmitMatchCharacterClass(primarySet.Set);
Brtrue(doneSearch);
// i++;
Ldloc(i);
Ldc(1);
Add();
Stloc(i);
// } while ((uint)i < span.Length);
Ldloc(i);
Ldloca(span);
Call(SpanGetLengthMethod);
BltUnFar(loop);
// i = -1;
Ldc(-1);
Stloc(i);
MarkLabel(doneSearch);
Ldloc(i);
}
}
if (needLoop)
{
// i += tmp;
// if (tmp < 0) goto returnFalse;
using (RentedLocalBuilder tmp = RentInt32Local())
{
Stloc(tmp);
Ldloc(iLocal);
Ldloc(tmp);
Add();
Stloc(iLocal);
Ldloc(tmp);
Ldc(0);
BltFar(returnFalse);
}
}
else
{
// i = tmp;
// if (i < 0) goto returnFalse;
Stloc(iLocal);
Ldloc(iLocal);
Ldc(0);
BltFar(returnFalse);
}
if (setsToUse > 1)
{
// Of the remaining sets we're going to check, find the maximum distance of any of them.
// If it's further than the primary set we checked, we need a bounds check.
int maxDistance = sets[1].Distance;
for (int i = 2; i < setsToUse; i++)
{
maxDistance = Math.Max(maxDistance, sets[i].Distance);
}
if (maxDistance > primarySet.Distance)
{
// if ((uint)(i + maxDistance) >= slice.Length) goto returnFalse;
if (setsToUse > 1)
{
Debug.Assert(needLoop);
Ldloc(iLocal);
Ldc(maxDistance);
Add();
Ldloca(textSpanLocal);
Call(SpanGetLengthMethod);
_ilg!.Emit(OpCodes.Bge_Un, returnFalse);
}
}
}
}
// if (!CharInClass(slice[i], prefix[0], "...")) continue;
// if (!CharInClass(slice[i + 1], prefix[1], "...")) continue;
// if (!CharInClass(slice[i + 2], prefix[2], "...")) continue;
// ...
Debug.Assert(setIndex is 0 or 1);
for (; setIndex < setsToUse; setIndex++)
{
Debug.Assert(needLoop);
Ldloca(textSpanLocal);
Ldloc(iLocal);
if (sets[setIndex].Distance != 0)
{
Ldc(sets[setIndex].Distance);
Add();
}
Call(SpanGetItemMethod);
LdindU2();
EmitMatchCharacterClass(sets[setIndex].Set);
BrfalseFar(charNotInClassLabel);
}
// base.runtextpos = pos + i;
// return true;
Ldthis();
Ldloc(pos);
Ldloc(iLocal);
Add();
Stfld(RuntextposField);
Ldc(1);
Ret();
if (needLoop)
{
MarkLabel(charNotInClassLabel);
// for (...; ...; i++)
Ldloc(iLocal);
Ldc(1);
Add();
Stloc(iLocal);
// for (...; i < span.Length - (minRequiredLength - 1); ...);
MarkLabel(checkSpanLengthLabel);
Ldloc(iLocal);
Ldloca(textSpanLocal);
Call(SpanGetLengthMethod);
if (setsToUse > 1 || primarySet.Distance != 0)
{
Ldc(minRequiredLength - 1);
Sub();
}
BltFar(loopBody);
// base.runtextpos = inputSpan.Length;
// return false;
BrFar(returnFalse);
}
}
// Emits a right-to-left search for a set at a fixed position from the start of the pattern.
// (Currently that position will always be a distance of 0, meaning the start of the pattern itself.)
void EmitFixedSet_RightToLeft()
{
Debug.Assert(_regexTree.FindOptimizations.FixedDistanceSets is { Count: > 0 });
RegexFindOptimizations.FixedDistanceSet set = _regexTree.FindOptimizations.FixedDistanceSets![0];
Debug.Assert(set.Distance == 0);
if (set.Chars is { Length: 1 })
{
Debug.Assert(!set.Negated);
// pos = inputSpan.Slice(0, pos).LastIndexOf(set.Chars[0]);
Ldloca(inputSpan);
Ldc(0);
Ldloc(pos);
Call(SpanSliceIntIntMethod);
Ldc(set.Chars[0]);
Call(SpanLastIndexOfCharMethod);
Stloc(pos);
// if (pos < 0) goto returnFalse;
Ldloc(pos);
Ldc(0);
BltFar(returnFalse);
// base.runtextpos = pos + 1;
// return true;
Ldthis();
Ldloc(pos);
Ldc(1);
Add();
Stfld(RuntextposField);
Ldc(1);
Ret();
}
else
{
Label condition = DefineLabel();
// while ((uint)--pos < (uint)inputSpan.Length)
MarkLabel(condition);
Ldloc(pos);
Ldc(1);
Sub();
Stloc(pos);
Ldloc(pos);
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
BgeUnFar(returnFalse);
// if (!MatchCharacterClass(inputSpan[i], set.Set)) goto condition;
Ldloca(inputSpan);
Ldloc(pos);
Call(SpanGetItemMethod);
LdindU2();
EmitMatchCharacterClass(set.Set);
Brfalse(condition);
// base.runtextpos = pos + 1;
// return true;
Ldthis();
Ldloc(pos);
Ldc(1);
Add();
Stfld(RuntextposField);
Ldc(1);
Ret();
}
}
// Emits a search for a literal following a leading atomic single-character loop.
void EmitLiteralAfterAtomicLoop()
{
Debug.Assert(_regexTree.FindOptimizations.LiteralAfterLoop is not null);
(RegexNode LoopNode, (char Char, string? String, StringComparison StringComparison, char[]? Chars) Literal) target = _regexTree.FindOptimizations.LiteralAfterLoop.Value;
Debug.Assert(target.LoopNode.Kind is RegexNodeKind.Setloop or RegexNodeKind.Setlazy or RegexNodeKind.Setloopatomic);
Debug.Assert(target.LoopNode.N == int.MaxValue);
// while (true)
Label loopBody = DefineLabel();
Label loopEnd = DefineLabel();
MarkLabel(loopBody);
// ReadOnlySpan<char> slice = inputSpan.Slice(pos);
using RentedLocalBuilder slice = RentReadOnlySpanCharLocal();
Ldloca(inputSpan);
Ldloc(pos);
Call(SpanSliceIntMethod);
Stloc(slice);
// Find the literal. If we can't find it, we're done searching.
// int i = slice.IndexOf(literal);
// if (i < 0) break;
using RentedLocalBuilder i = RentInt32Local();
Ldloc(slice);
if (target.Literal.String is string literalString)
{
Ldstr(literalString);
Call(StringAsSpanMethod);
if (target.Literal.StringComparison is StringComparison.OrdinalIgnoreCase)
{
Ldc((int)target.Literal.StringComparison);
Call(SpanIndexOfSpanStringComparisonMethod);
}
else
{
Debug.Assert(target.Literal.StringComparison is StringComparison.Ordinal);
Call(SpanIndexOfSpanMethod);
}
}
else if (target.Literal.Chars is not char[] literalChars)
{
Ldc(target.Literal.Char);
Call(SpanIndexOfCharMethod);
}
else
{
switch (literalChars.Length)
{
case 2:
Ldc(literalChars[0]);
Ldc(literalChars[1]);
Call(SpanIndexOfAnyCharCharMethod);
break;
case 3:
Ldc(literalChars[0]);
Ldc(literalChars[1]);
Ldc(literalChars[2]);
Call(SpanIndexOfAnyCharCharCharMethod);
break;
default:
Ldstr(new string(literalChars));
Call(StringAsSpanMethod);
Call(SpanIndexOfAnySpanMethod);
break;
}
}
Stloc(i);
Ldloc(i);
Ldc(0);
BltFar(loopEnd);
// We found the literal. Walk backwards from it finding as many matches as we can against the loop.
// int prev = i;
using RentedLocalBuilder prev = RentInt32Local();
Ldloc(i);
Stloc(prev);
// while ((uint)--prev < (uint)slice.Length) && MatchCharClass(slice[prev]));
Label innerLoopBody = DefineLabel();
Label innerLoopEnd = DefineLabel();
MarkLabel(innerLoopBody);
Ldloc(prev);
Ldc(1);
Sub();
Stloc(prev);
Ldloc(prev);
Ldloca(slice);
Call(SpanGetLengthMethod);
BgeUn(innerLoopEnd);
Ldloca(slice);
Ldloc(prev);
Call(SpanGetItemMethod);
LdindU2();
EmitMatchCharacterClass(target.LoopNode.Str!);
BrtrueFar(innerLoopBody);
MarkLabel(innerLoopEnd);
if (target.LoopNode.M > 0)
{
// If we found fewer than needed, loop around to try again. The loop doesn't overlap with the literal,
// so we can start from after the last place the literal matched.
// if ((i - prev - 1) < target.LoopNode.M)
// {
// pos += i + 1;
// continue;
// }
Label metMinimum = DefineLabel();
Ldloc(i);
Ldloc(prev);
Sub();
Ldc(1);
Sub();
Ldc(target.LoopNode.M);
Bge(metMinimum);
Ldloc(pos);
Ldloc(i);
Add();
Ldc(1);
Add();
Stloc(pos);
BrFar(loopBody);
MarkLabel(metMinimum);
}
// We have a winner. The starting position is just after the last position that failed to match the loop.
// We also store the position after the loop into runtrackpos (an extra, unused field on RegexRunner) in order
// to communicate this position to the match algorithm such that it can skip the loop.
// base.runtextpos = pos + prev + 1;
Ldthis();
Ldloc(pos);
Ldloc(prev);
Add();
Ldc(1);
Add();
Stfld(RuntextposField);
// base.runtrackpos = pos + i;
Ldthis();
Ldloc(pos);
Ldloc(i);
Add();
Stfld(RuntrackposField);
// return true;
Ldc(1);
Ret();
// }
MarkLabel(loopEnd);
// base.runtextpos = inputSpan.Length;
// return false;
BrFar(returnFalse);
}
}
/// <summary>Generates the implementation for TryMatchAtCurrentPosition.</summary>
protected void EmitTryMatchAtCurrentPosition()
{
// In .NET Framework and up through .NET Core 3.1, the code generated for RegexOptions.Compiled was effectively an unrolled
// version of what RegexInterpreter would process. The RegexNode tree would be turned into a series of opcodes via
// RegexWriter; the interpreter would then sit in a loop processing those opcodes, and the RegexCompiler iterated through the
// opcodes generating code for each equivalent to what the interpreter would do albeit with some decisions made at compile-time
// rather than at run-time. This approach, however, lead to complicated code that wasn't pay-for-play (e.g. a big backtracking
// jump table that all compilations went through even if there was no backtracking), that didn't factor in the shape of the
// tree (e.g. it's difficult to add optimizations based on interactions between nodes in the graph), and that didn't read well
// when decompiled from IL to C# or when directly emitted as C# as part of a source generator.
//
// This implementation is instead based on directly walking the RegexNode tree and outputting code for each node in the graph.
// A dedicated for each kind of RegexNode emits the code necessary to handle that node's processing, including recursively
// calling the relevant function for any of its children nodes. Backtracking is handled not via a giant jump table, but instead
// by emitting direct jumps to each backtracking construct. This is achieved by having all match failures jump to a "done"
// label that can be changed by a previous emitter, e.g. before EmitLoop returns, it ensures that "doneLabel" is set to the
// label that code should jump back to when backtracking. That way, a subsequent EmitXx function doesn't need to know exactly
// where to jump: it simply always jumps to "doneLabel" on match failure, and "doneLabel" is always configured to point to
// the right location. In an expression without backtracking, or before any backtracking constructs have been encountered,
// "doneLabel" is simply the final return location from the TryMatchAtCurrentPosition method that will undo any captures and exit, signaling to
// the calling scan loop that nothing was matched.
Debug.Assert(_regexTree != null);
_int32LocalsPool?.Clear();
_readOnlySpanCharLocalsPool?.Clear();
// Get the root Capture node of the tree.
RegexNode node = _regexTree.Root;
Debug.Assert(node.Kind == RegexNodeKind.Capture, "Every generated tree should begin with a capture node");
Debug.Assert(node.ChildCount() == 1, "Capture nodes should have one child");
// Skip the Capture node. We handle the implicit root capture specially.
node = node.Child(0);
// In some limited cases, TryFindNextPossibleStartingPosition will only return true if it successfully matched the whole expression.
// We can special case these to do essentially nothing in TryMatchAtCurrentPosition other than emit the capture.
switch (node.Kind)
{
case RegexNodeKind.Multi or RegexNodeKind.Notone or RegexNodeKind.One or RegexNodeKind.Set:
// This is the case for single and multiple characters, though the whole thing is only guaranteed
// to have been validated in TryFindNextPossibleStartingPosition when doing case-sensitive comparison.
// base.Capture(0, base.runtextpos, base.runtextpos + node.Str.Length);
// base.runtextpos = base.runtextpos + node.Str.Length;
// return true;
int length = node.Kind == RegexNodeKind.Multi ? node.Str!.Length : 1;
if ((node.Options & RegexOptions.RightToLeft) != 0)
{
length = -length;
}
Ldthis();
Dup();
Ldc(0);
Ldthisfld(RuntextposField);
Dup();
Ldc(length);
Add();
Call(CaptureMethod);
Ldthisfld(RuntextposField);
Ldc(length);
Add();
Stfld(RuntextposField);
Ldc(1);
Ret();
return;
// The source generator special-cases RegexNode.Empty, for purposes of code learning rather than
// performance. Since that's not applicable to RegexCompiler, that code isn't mirrored here.
}
AnalysisResults analysis = RegexTreeAnalyzer.Analyze(_regexTree);
// Initialize the main locals used throughout the implementation.
LocalBuilder inputSpan = DeclareReadOnlySpanChar();
LocalBuilder originalPos = DeclareInt32();
LocalBuilder pos = DeclareInt32();
LocalBuilder slice = DeclareReadOnlySpanChar();
Label doneLabel = DefineLabel();
Label originalDoneLabel = doneLabel;
// ReadOnlySpan<char> inputSpan = input;
Ldarg_1();
Stloc(inputSpan);
// int pos = base.runtextpos;
// int originalpos = pos;
Ldthisfld(RuntextposField);
Stloc(pos);
Ldloc(pos);
Stloc(originalPos);
// int stackpos = 0;
LocalBuilder stackpos = DeclareInt32();
Ldc(0);
Stloc(stackpos);
// The implementation tries to use const indexes into the span wherever possible, which we can do
// for all fixed-length constructs. In such cases (e.g. single chars, repeaters, strings, etc.)
// we know at any point in the regex exactly how far into it we are, and we can use that to index
// into the span created at the beginning of the routine to begin at exactly where we're starting
// in the input. When we encounter a variable-length construct, we transfer the static value to
// pos, slicing the inputSpan appropriately, and then zero out the static position.
int sliceStaticPos = 0;
SliceInputSpan();
// Check whether there are captures anywhere in the expression. If there isn't, we can skip all
// the boilerplate logic around uncapturing, as there won't be anything to uncapture.
bool expressionHasCaptures = analysis.MayContainCapture(node);
// Emit the code for all nodes in the tree.
EmitNode(node);
// pos += sliceStaticPos;
// base.runtextpos = pos;
// Capture(0, originalpos, pos);
// return true;
Ldthis();
Ldloc(pos);
if (sliceStaticPos > 0)
{
Ldc(sliceStaticPos);
Add();
Stloc(pos);
Ldloc(pos);
}
Stfld(RuntextposField);
Ldthis();
Ldc(0);
Ldloc(originalPos);
Ldloc(pos);
Call(CaptureMethod);
Ldc(1);
Ret();
// NOTE: The following is a difference from the source generator. The source generator emits:
// UncaptureUntil(0);
// return false;
// at every location where the all-up match is known to fail. In contrast, the compiler currently
// emits this uncapture/return code in one place and jumps to it upon match failure. The difference
// stems primarily from the return-at-each-location pattern resulting in cleaner / easier to read
// source code, which is not an issue for RegexCompiler emitting IL instead of C#.
// If the graph contained captures, undo any remaining to handle failed matches.
if (expressionHasCaptures)
{
// while (base.Crawlpos() != 0) base.Uncapture();
Label finalReturnLabel = DefineLabel();
Br(finalReturnLabel);
MarkLabel(originalDoneLabel);
Label condition = DefineLabel();
Label body = DefineLabel();
Br(condition);
MarkLabel(body);
Ldthis();
Call(UncaptureMethod);
MarkLabel(condition);
Ldthis();
Call(CrawlposMethod);
Brtrue(body);
// Done:
MarkLabel(finalReturnLabel);
}
else
{
// Done:
MarkLabel(originalDoneLabel);
}
// return false;
Ldc(0);
Ret();
// Generated code successfully.
return;
// Slices the inputSpan starting at pos until end and stores it into slice.
void SliceInputSpan()
{
// slice = inputSpan.Slice(pos);
Ldloca(inputSpan);
Ldloc(pos);
Call(SpanSliceIntMethod);
Stloc(slice);
}
// Emits the sum of a constant and a value from a local.
void EmitSum(int constant, LocalBuilder? local)
{
if (local == null)
{
Ldc(constant);
}
else if (constant == 0)
{
Ldloc(local);
}
else
{
Ldloc(local);
Ldc(constant);
Add();
}
}
// Emits a check that the span is large enough at the currently known static position to handle the required additional length.
void EmitSpanLengthCheck(int requiredLength, LocalBuilder? dynamicRequiredLength = null)
{
// if ((uint)(sliceStaticPos + requiredLength + dynamicRequiredLength - 1) >= (uint)slice.Length) goto Done;
Debug.Assert(requiredLength > 0);
EmitSum(sliceStaticPos + requiredLength - 1, dynamicRequiredLength);
Ldloca(slice);
Call(SpanGetLengthMethod);
BgeUnFar(doneLabel);
}
// Adds the value of sliceStaticPos into the pos local, zeros out sliceStaticPos,
// and resets slice to be inputSpan.Slice(pos).
void TransferSliceStaticPosToPos(bool forceSliceReload = false)
{
if (sliceStaticPos > 0)
{
// pos += sliceStaticPos;
// sliceStaticPos = 0;
Ldloc(pos);
Ldc(sliceStaticPos);
Add();
Stloc(pos);
sliceStaticPos = 0;
// slice = inputSpan.Slice(pos);
SliceInputSpan();
}
else if (forceSliceReload)
{
// slice = inputSpan.Slice(pos);
SliceInputSpan();
}
}
// Emits the code for an alternation.
void EmitAlternation(RegexNode node)
{
Debug.Assert(node.Kind is RegexNodeKind.Alternate, $"Unexpected type: {node.Kind}");
Debug.Assert(node.ChildCount() >= 2, $"Expected at least 2 children, found {node.ChildCount()}");
int childCount = node.ChildCount();
Debug.Assert(childCount >= 2);
Label originalDoneLabel = doneLabel;
// Both atomic and non-atomic are supported. While a parent RegexNode.Atomic node will itself
// successfully prevent backtracking into this child node, we can emit better / cheaper code
// for an Alternate when it is atomic, so we still take it into account here.
Debug.Assert(node.Parent is not null);
bool isAtomic = analysis.IsAtomicByAncestor(node);
// Label to jump to when any branch completes successfully.
Label matchLabel = DefineLabel();
// Save off pos. We'll need to reset this each time a branch fails.
// startingPos = pos;
LocalBuilder startingPos = DeclareInt32();
Ldloc(pos);
Stloc(startingPos);
int startingTextSpanPos = sliceStaticPos;
// We need to be able to undo captures in two situations:
// - If a branch of the alternation itself contains captures, then if that branch
// fails to match, any captures from that branch until that failure point need to
// be uncaptured prior to jumping to the next branch.
// - If the expression after the alternation contains captures, then failures
// to match in those expressions could trigger backtracking back into the
// alternation, and thus we need uncapture any of them.
// As such, if the alternation contains captures or if it's not atomic, we need
// to grab the current crawl position so we can unwind back to it when necessary.
// We can do all of the uncapturing as part of falling through to the next branch.
// If we fail in a branch, then such uncapturing will unwind back to the position
// at the start of the alternation. If we fail after the alternation, and the
// matched branch didn't contain any backtracking, then the failure will end up
// jumping to the next branch, which will unwind the captures. And if we fail after
// the alternation and the matched branch did contain backtracking, that backtracking
// construct is responsible for unwinding back to its starting crawl position. If
// it eventually ends up failing, that failure will result in jumping to the next branch
// of the alternation, which will again dutifully unwind the remaining captures until
// what they were at the start of the alternation. Of course, if there are no captures
// anywhere in the regex, we don't have to do any of that.
LocalBuilder? startingCapturePos = null;
if (expressionHasCaptures && (analysis.MayContainCapture(node) || !isAtomic))
{
// startingCapturePos = base.Crawlpos();
startingCapturePos = DeclareInt32();
Ldthis();
Call(CrawlposMethod);
Stloc(startingCapturePos);
}
// After executing the alternation, subsequent matching may fail, at which point execution
// will need to backtrack to the alternation. We emit a branching table at the end of the
// alternation, with a label that will be left as the "doneLabel" upon exiting emitting the
// alternation. The branch table is populated with an entry for each branch of the alternation,
// containing either the label for the last backtracking construct in the branch if such a construct
// existed (in which case the doneLabel upon emitting that node will be different from before it)
// or the label for the next branch.
bool canUseLocalsForAllState = !isAtomic && !analysis.IsInLoop(node);
var labelMap = new Label[childCount];
Label backtrackLabel = DefineLabel();
LocalBuilder? currentBranch = canUseLocalsForAllState ? DeclareInt32() : null;
for (int i = 0; i < childCount; i++)
{
bool isLastBranch = i == childCount - 1;
Label nextBranch = default;
if (!isLastBranch)
{
// Failure to match any branch other than the last one should result
// in jumping to process the next branch.
nextBranch = DefineLabel();
doneLabel = nextBranch;
}
else
{
// Failure to match the last branch is equivalent to failing to match
// the whole alternation, which means those failures should jump to
// what "doneLabel" was defined as when starting the alternation.
doneLabel = originalDoneLabel;
}
// Emit the code for each branch.
EmitNode(node.Child(i));
// Add this branch to the backtracking table. At this point, either the child
// had backtracking constructs, in which case doneLabel points to the last one
// and that's where we'll want to jump to, or it doesn't, in which case doneLabel
// still points to the nextBranch, which similarly is where we'll want to jump to.
if (!isAtomic)
{
// If we're inside of a loop, push the state we need to preserve on to the
// the backtracking stack. If we're not inside of a loop, simply ensure all
// the relevant state is stored in our locals.
if (currentBranch is null)
{
// if (stackpos + 3 >= base.runstack.Length) Array.Resize(ref base.runstack, base.runstack.Length * 2);
// base.runstack[stackpos++] = i;
// base.runstack[stackpos++] = startingCapturePos;
// base.runstack[stackpos++] = startingPos;
EmitStackResizeIfNeeded(2 + (startingCapturePos is not null ? 1 : 0));
EmitStackPush(() => Ldc(i));
if (startingCapturePos is not null)
{
EmitStackPush(() => Ldloc(startingCapturePos));
}
EmitStackPush(() => Ldloc(startingPos));
}
else
{
// currentBranch = i;
Ldc(i);
Stloc(currentBranch);
}
}
labelMap[i] = doneLabel;
// If we get here in the generated code, the branch completed successfully.
// Before jumping to the end, we need to zero out sliceStaticPos, so that no
// matter what the value is after the branch, whatever follows the alternate
// will see the same sliceStaticPos.
// pos += sliceStaticPos;
// sliceStaticPos = 0;
// goto matchLabel;
TransferSliceStaticPosToPos();
BrFar(matchLabel);
// Reset state for next branch and loop around to generate it. This includes
// setting pos back to what it was at the beginning of the alternation,
// updating slice to be the full length it was, and if there's a capture that
// needs to be reset, uncapturing it.
if (!isLastBranch)
{
// NextBranch:
// pos = startingPos;
// slice = inputSpan.Slice(pos);
// while (base.Crawlpos() > startingCapturePos) base.Uncapture();
MarkLabel(nextBranch);
Ldloc(startingPos);
Stloc(pos);
SliceInputSpan();
sliceStaticPos = startingTextSpanPos;
if (startingCapturePos is not null)
{
EmitUncaptureUntil(startingCapturePos);
}
}
}
// We should never fall through to this location in the generated code. Either
// a branch succeeded in matching and jumped to the end, or a branch failed in
// matching and jumped to the next branch location. We only get to this code
// if backtracking occurs and the code explicitly jumps here based on our setting
// "doneLabel" to the label for this section. Thus, we only need to emit it if
// something can backtrack to us, which can't happen if we're inside of an atomic
// node. Thus, emit the backtracking section only if we're non-atomic.
if (isAtomic)
{
doneLabel = originalDoneLabel;
}
else
{
doneLabel = backtrackLabel;
MarkLabel(backtrackLabel);
// We're backtracking. Check the timeout.
EmitTimeoutCheckIfNeeded();
if (currentBranch is null)
{
// We're in a loop, so we use the backtracking stack to persist our state. Pop it off.
// startingPos = base.runstack[--stackpos];
// startingCapturePos = base.runstack[--stackpos];
// switch (base.runstack[--stackpos])
EmitStackPop();
Stloc(startingPos);
if (startingCapturePos is not null)
{
EmitStackPop();
Stloc(startingCapturePos);
}
EmitStackPop();
}
else
{
// We're not in a loop, so our locals already store the state we need.
// switch (currentBranch)
Ldloc(currentBranch);
}
Switch(labelMap);
}
// Successfully completed the alternate.
MarkLabel(matchLabel);
Debug.Assert(sliceStaticPos == 0);
}
// Emits the code to handle a backreference.
void EmitBackreference(RegexNode node)
{
Debug.Assert(node.Kind is RegexNodeKind.Backreference, $"Unexpected type: {node.Kind}");
int capnum = RegexParser.MapCaptureNumber(node.M, _regexTree!.CaptureNumberSparseMapping);
bool rtl = (node.Options & RegexOptions.RightToLeft) != 0;
TransferSliceStaticPosToPos();
Label backreferenceEnd = DefineLabel();
// if (!base.IsMatched(capnum)) goto (ecmascript ? end : doneLabel);
Ldthis();
Ldc(capnum);
Call(IsMatchedMethod);
BrfalseFar((node.Options & RegexOptions.ECMAScript) == 0 ? doneLabel : backreferenceEnd);
using RentedLocalBuilder matchLength = RentInt32Local();
using RentedLocalBuilder matchIndex = RentInt32Local();
using RentedLocalBuilder i = RentInt32Local();
// int matchLength = base.MatchLength(capnum);
Ldthis();
Ldc(capnum);
Call(MatchLengthMethod);
Stloc(matchLength);
if (!rtl)
{
// if (slice.Length < matchLength) goto doneLabel;
Ldloca(slice);
Call(SpanGetLengthMethod);
}
else
{
// if (pos < matchLength) goto doneLabel;
Ldloc(pos);
}
Ldloc(matchLength);
BltFar(doneLabel);
// int matchIndex = base.MatchIndex(capnum);
Ldthis();
Ldc(capnum);
Call(MatchIndexMethod);
Stloc(matchIndex);
Label condition = DefineLabel();
Label body = DefineLabel();
Label charactersMatched = DefineLabel();
LocalBuilder backreferenceCharacter = _ilg!.DeclareLocal(typeof(char));
LocalBuilder currentCharacter = _ilg.DeclareLocal(typeof(char));
// for (int i = 0; ...)
Ldc(0);
Stloc(i);
Br(condition);
MarkLabel(body);
// char backreferenceChar = inputSpan[matchIndex + i];
Ldloca(inputSpan);
Ldloc(matchIndex);
Ldloc(i);
Add();
Call(SpanGetItemMethod);
LdindU2();
Stloc(backreferenceCharacter);
if (!rtl)
{
// char currentChar = slice[i];
Ldloca(slice);
Ldloc(i);
}
else
{
// char currentChar = inputSpan[pos - matchLength + i];
Ldloca(inputSpan);
Ldloc(pos);
Ldloc(matchLength);
Sub();
Ldloc(i);
Add();
}
Call(SpanGetItemMethod);
LdindU2();
Stloc(currentCharacter);
if ((node.Options & RegexOptions.IgnoreCase) != 0)
{
LocalBuilder caseEquivalences = DeclareReadOnlySpanChar();
// if (backreferenceChar != currentChar)
Ldloc(backreferenceCharacter);
Ldloc(currentCharacter);
Ceq();
BrtrueFar(charactersMatched);
// if (RegexCaseEquivalences.TryFindCaseEquivalencesForCharWithIBehavior(backreferenceChar, _culture, ref _caseBehavior, out ReadOnlySpan<char> equivalences))
Ldloc(backreferenceCharacter);
Ldthisfld(CultureField);
Ldthisflda(CaseBehaviorField);
Ldloca(caseEquivalences);
Call(RegexCaseEquivalencesTryFindCaseEquivalencesForCharWithIBehaviorMethod);
BrfalseFar(doneLabel);
// if (equivalences.IndexOf(slice[i]) < 0) // Or if (equivalences.IndexOf(inputSpan[pos - matchLength + i]) < 0) when rtl
Ldloc(caseEquivalences);
if (!rtl)
{
Ldloca(slice);
Ldloc(i);
}
else
{
Ldloca(inputSpan);
Ldloc(pos);
Ldloc(matchLength);
Sub();
Ldloc(i);
Add();
}
Call(SpanGetItemMethod);
LdindU2();
Call(SpanIndexOfCharMethod);
Ldc(0);
// return false; // input didn't match.
BltFar(doneLabel);
}
else
{
// if (backreferenceCharacter != currentCharacter)
Ldloc(backreferenceCharacter);
Ldloc(currentCharacter);
Ceq();
// return false; // input didn't match.
BrfalseFar(doneLabel);
}
MarkLabel(charactersMatched);
// for (...; ...; i++)
Ldloc(i);
Ldc(1);
Add();
Stloc(i);
// for (...; i < matchLength; ...)
MarkLabel(condition);
Ldloc(i);
Ldloc(matchLength);
Blt(body);
// pos += matchLength; // or -= for rtl
Ldloc(pos);
Ldloc(matchLength);
if (!rtl)
{
Add();
}
else
{
Sub();
}
Stloc(pos);
if (!rtl)
{
SliceInputSpan();
}
MarkLabel(backreferenceEnd);
}
// Emits the code for an if(backreference)-then-else conditional.
void EmitBackreferenceConditional(RegexNode node)
{
Debug.Assert(node.Kind is RegexNodeKind.BackreferenceConditional, $"Unexpected type: {node.Kind}");
Debug.Assert(node.ChildCount() == 2, $"Expected 2 children, found {node.ChildCount()}");
bool isAtomic = analysis.IsAtomicByAncestor(node);
// We're branching in a complicated fashion. Make sure sliceStaticPos is 0.
TransferSliceStaticPosToPos();
// Get the capture number to test.
int capnum = RegexParser.MapCaptureNumber(node.M, _regexTree!.CaptureNumberSparseMapping);
// Get the "yes" branch and the "no" branch. The "no" branch is optional in syntax and is thus
// somewhat likely to be Empty.
RegexNode yesBranch = node.Child(0);
RegexNode? noBranch = node.Child(1) is { Kind: not RegexNodeKind.Empty } childNo ? childNo : null;
Label originalDoneLabel = doneLabel;
Label refNotMatched = DefineLabel();
Label endConditional = DefineLabel();
// As with alternations, we have potentially multiple branches, each of which may contain
// backtracking constructs, but the expression after the conditional needs a single target
// to backtrack to. So, we expose a single Backtrack label and track which branch was
// followed in this resumeAt local.
LocalBuilder resumeAt = DeclareInt32();
bool isInLoop = analysis.IsInLoop(node);
// if (!base.IsMatched(capnum)) goto refNotMatched;
Ldthis();
Ldc(capnum);
Call(IsMatchedMethod);
BrfalseFar(refNotMatched);
// The specified capture was captured. Run the "yes" branch.
// If it successfully matches, jump to the end.
EmitNode(yesBranch);
TransferSliceStaticPosToPos();
Label postYesDoneLabel = doneLabel;
if ((!isAtomic && postYesDoneLabel != originalDoneLabel) || isInLoop)
{
// resumeAt = 0;
Ldc(0);
Stloc(resumeAt);
}
bool needsEndConditional = postYesDoneLabel != originalDoneLabel || noBranch is not null;
if (needsEndConditional)
{
// goto endConditional;
BrFar(endConditional);
}
MarkLabel(refNotMatched);
Label postNoDoneLabel = originalDoneLabel;
if (noBranch is not null)
{
// Output the no branch.
doneLabel = originalDoneLabel;
EmitNode(noBranch);
TransferSliceStaticPosToPos(); // make sure sliceStaticPos is 0 after each branch
postNoDoneLabel = doneLabel;
if ((!isAtomic && postNoDoneLabel != originalDoneLabel) || isInLoop)
{
// resumeAt = 1;
Ldc(1);
Stloc(resumeAt);
}
}
else
{
// There's only a yes branch. If it's going to cause us to output a backtracking
// label but code may not end up taking the yes branch path, we need to emit a resumeAt
// that will cause the backtracking to immediately pass through this node.
if ((!isAtomic && postYesDoneLabel != originalDoneLabel) || isInLoop)
{
// resumeAt = 2;
Ldc(2);
Stloc(resumeAt);
}
}
if (isAtomic || (postYesDoneLabel == originalDoneLabel && postNoDoneLabel == originalDoneLabel))
{
// We're atomic by our parent, so even if either child branch has backtracking constructs,
// we don't need to emit any backtracking logic in support, as nothing will backtrack in.
// Instead, we just ensure we revert back to the original done label so that any backtracking
// skips over this node.
doneLabel = originalDoneLabel;
if (needsEndConditional)
{
MarkLabel(endConditional);
}
}
else
{
// Subsequent expressions might try to backtrack to here, so output a backtracking map based on resumeAt.
// Skip the backtracking section
// goto endConditional;
Debug.Assert(needsEndConditional);
Br(endConditional);
// Backtrack section
Label backtrack = DefineLabel();
doneLabel = backtrack;
MarkLabel(backtrack);
// Pop from the stack the branch that was used and jump back to its backtracking location.
// If we're not in a loop, though, we won't have pushed it on to the stack as nothing will
// have been able to overwrite it in the interim, so we can just trust the value already in
// the local.
if (isInLoop)
{
// resumeAt = base.runstack[--stackpos];
EmitStackPop();
Stloc(resumeAt);
}
if (postYesDoneLabel != originalDoneLabel)
{
// if (resumeAt == 0) goto postIfDoneLabel;
Ldloc(resumeAt);
Ldc(0);
BeqFar(postYesDoneLabel);
}
if (postNoDoneLabel != originalDoneLabel)
{
// if (resumeAt == 1) goto postNoDoneLabel;
Ldloc(resumeAt);
Ldc(1);
BeqFar(postNoDoneLabel);
}
// goto originalDoneLabel;
BrFar(originalDoneLabel);
if (needsEndConditional)
{
MarkLabel(endConditional);
}
if (isInLoop)
{
// We're not atomic and at least one of the yes or no branches contained backtracking constructs,
// so finish outputting our backtracking logic, which involves pushing onto the stack which
// branch to backtrack into. If we're not in a loop, though, nothing else can overwrite this local
// in the interim, so we can avoid pushing it.
// if (stackpos + 1 >= base.runstack.Length) Array.Resize(ref base.runstack, base.runstack.Length * 2);
// base.runstack[stackpos++] = resumeAt;
EmitStackResizeIfNeeded(1);
EmitStackPush(() => Ldloc(resumeAt));
}
}
}
// Emits the code for an if(expression)-then-else conditional.
void EmitExpressionConditional(RegexNode node)
{
Debug.Assert(node.Kind is RegexNodeKind.ExpressionConditional, $"Unexpected type: {node.Kind}");
Debug.Assert(node.ChildCount() == 3, $"Expected 3 children, found {node.ChildCount()}");
bool isAtomic = analysis.IsAtomicByAncestor(node);
// We're branching in a complicated fashion. Make sure sliceStaticPos is 0.
TransferSliceStaticPosToPos();
// The first child node is the condition expression. If this matches, then we branch to the "yes" branch.
// If it doesn't match, then we branch to the optional "no" branch if it exists, or simply skip the "yes"
// branch, otherwise. The condition is treated as a positive lookaround.
RegexNode condition = node.Child(0);
// Get the "yes" branch and the "no" branch. The "no" branch is optional in syntax and is thus
// somewhat likely to be Empty.
RegexNode yesBranch = node.Child(1);
RegexNode? noBranch = node.Child(2) is { Kind: not RegexNodeKind.Empty } childNo ? childNo : null;
Label originalDoneLabel = doneLabel;
Label expressionNotMatched = DefineLabel();
Label endConditional = DefineLabel();
// As with alternations, we have potentially multiple branches, each of which may contain
// backtracking constructs, but the expression after the condition needs a single target
// to backtrack to. So, we expose a single Backtrack label and track which branch was
// followed in this resumeAt local.
bool isInLoop = false;
LocalBuilder? resumeAt = null;
if (!isAtomic)
{
isInLoop = analysis.IsInLoop(node);
resumeAt = DeclareInt32();
}
// If the condition expression has captures, we'll need to uncapture them in the case of no match.
LocalBuilder? startingCapturePos = null;
if (analysis.MayContainCapture(condition))
{
// int startingCapturePos = base.Crawlpos();
startingCapturePos = DeclareInt32();
Ldthis();
Call(CrawlposMethod);
Stloc(startingCapturePos);
}
// Emit the condition expression. Route any failures to after the yes branch. This code is almost
// the same as for a positive lookaround; however, a positive lookaround only needs to reset the position
// on a successful match, as a failed match fails the whole expression; here, we need to reset the
// position on completion, regardless of whether the match is successful or not.
doneLabel = expressionNotMatched;
// Save off pos. We'll need to reset this upon successful completion of the lookaround.
// startingPos = pos;
LocalBuilder startingPos = DeclareInt32();
Ldloc(pos);
Stloc(startingPos);
int startingSliceStaticPos = sliceStaticPos;
// Emit the condition. The condition expression is a zero-width assertion, which is atomic,
// so prevent backtracking into it.
if (analysis.MayBacktrack(condition))
{
// Condition expressions are treated like positive lookarounds and thus are implicitly atomic,
// so we need to emit the node as atomic if it might backtrack.
EmitAtomic(node, null);
}
else
{
EmitNode(condition);
}
doneLabel = originalDoneLabel;
// After the condition completes successfully, reset the text positions.
// Do not reset captures, which persist beyond the lookaround.
// pos = startingPos;
// slice = inputSpan.Slice(pos);
Ldloc(startingPos);
Stloc(pos);
SliceInputSpan();
sliceStaticPos = startingSliceStaticPos;
// The expression matched. Run the "yes" branch. If it successfully matches, jump to the end.
EmitNode(yesBranch);
TransferSliceStaticPosToPos(); // make sure sliceStaticPos is 0 after each branch
Label postYesDoneLabel = doneLabel;
if (!isAtomic && postYesDoneLabel != originalDoneLabel)
{
// resumeAt = 0;
Ldc(0);
Stloc(resumeAt!);
}
// goto endConditional;
BrFar(endConditional);
// After the condition completes unsuccessfully, reset the text positions
// _and_ reset captures, which should not persist when the whole expression failed.
// pos = startingPos;
MarkLabel(expressionNotMatched);
Ldloc(startingPos);
Stloc(pos);
SliceInputSpan();
sliceStaticPos = startingSliceStaticPos;
if (startingCapturePos is not null)
{
EmitUncaptureUntil(startingCapturePos);
}
Label postNoDoneLabel = originalDoneLabel;
if (noBranch is not null)
{
// Output the no branch.
doneLabel = originalDoneLabel;
EmitNode(noBranch);
TransferSliceStaticPosToPos(); // make sure sliceStaticPos is 0 after each branch
postNoDoneLabel = doneLabel;
if (!isAtomic && postNoDoneLabel != originalDoneLabel)
{
// resumeAt = 1;
Ldc(1);
Stloc(resumeAt!);
}
}
else
{
// There's only a yes branch. If it's going to cause us to output a backtracking
// label but code may not end up taking the yes branch path, we need to emit a resumeAt
// that will cause the backtracking to immediately pass through this node.
if (!isAtomic && postYesDoneLabel != originalDoneLabel)
{
// resumeAt = 2;
Ldc(2);
Stloc(resumeAt!);
}
}
// If either the yes branch or the no branch contained backtracking, subsequent expressions
// might try to backtrack to here, so output a backtracking map based on resumeAt.
if (isAtomic || (postYesDoneLabel == originalDoneLabel && postNoDoneLabel == originalDoneLabel))
{
// EndConditional:
doneLabel = originalDoneLabel;
MarkLabel(endConditional);
}
else
{
Debug.Assert(resumeAt is not null);
// Skip the backtracking section.
BrFar(endConditional);
Label backtrack = DefineLabel();
doneLabel = backtrack;
MarkLabel(backtrack);
if (isInLoop)
{
// If we're not in a loop, the local will maintain its value until backtracking occurs.
// If we are in a loop, multiple iterations need their own value, so we need to use the stack.
// resumeAt = StackPop();
EmitStackPop();
Stloc(resumeAt);
}
if (postYesDoneLabel != originalDoneLabel)
{
// if (resumeAt == 0) goto postYesDoneLabel;
Ldloc(resumeAt);
Ldc(0);
BeqFar(postYesDoneLabel);
}
if (postNoDoneLabel != originalDoneLabel)
{
// if (resumeAt == 1) goto postNoDoneLabel;
Ldloc(resumeAt);
Ldc(1);
BeqFar(postNoDoneLabel);
}
// goto postConditionalDoneLabel;
BrFar(originalDoneLabel);
// EndConditional:
MarkLabel(endConditional);
if (isInLoop)
{
// if (stackpos + 1 >= base.runstack.Length) Array.Resize(ref base.runstack, base.runstack.Length * 2);
// base.runstack[stackpos++] = resumeAt;
EmitStackResizeIfNeeded(1);
EmitStackPush(() => Ldloc(resumeAt!));
}
}
}
// Emits the code for a Capture node.
void EmitCapture(RegexNode node, RegexNode? subsequent = null)
{
Debug.Assert(node.Kind is RegexNodeKind.Capture, $"Unexpected type: {node.Kind}");
Debug.Assert(node.ChildCount() == 1, $"Expected 1 child, found {node.ChildCount()}");
int capnum = RegexParser.MapCaptureNumber(node.M, _regexTree!.CaptureNumberSparseMapping);
int uncapnum = RegexParser.MapCaptureNumber(node.N, _regexTree.CaptureNumberSparseMapping);
bool isAtomic = analysis.IsAtomicByAncestor(node);
bool isInLoop = analysis.IsInLoop(node);
// pos += sliceStaticPos;
// slice = slice.Slice(sliceStaticPos);
// startingPos = pos;
TransferSliceStaticPosToPos();
LocalBuilder startingPos = DeclareInt32();
Ldloc(pos);
Stloc(startingPos);
RegexNode child = node.Child(0);
if (uncapnum != -1)
{
// if (!IsMatched(uncapnum)) goto doneLabel;
Ldthis();
Ldc(uncapnum);
Call(IsMatchedMethod);
BrfalseFar(doneLabel);
}
// Emit child node.
Label originalDoneLabel = doneLabel;
EmitNode(child, subsequent);
bool childBacktracks = doneLabel != originalDoneLabel;
// pos += sliceStaticPos;
// slice = slice.Slice(sliceStaticPos);
TransferSliceStaticPosToPos();
if (uncapnum == -1)
{
// Capture(capnum, startingPos, pos);
Ldthis();
Ldc(capnum);
Ldloc(startingPos);
Ldloc(pos);
Call(CaptureMethod);
}
else
{
// TransferCapture(capnum, uncapnum, startingPos, pos);
Ldthis();
Ldc(capnum);
Ldc(uncapnum);
Ldloc(startingPos);
Ldloc(pos);
Call(TransferCaptureMethod);
}
if (isAtomic || !childBacktracks)
{
// If the capture is atomic and nothing can backtrack into it, we're done.
// Similarly, even if the capture isn't atomic, if the captured expression
// doesn't do any backtracking, we're done.
doneLabel = originalDoneLabel;
}
else
{
// We're not atomic and the child node backtracks. When it does, we need
// to ensure that the starting position for the capture is appropriately
// reset to what it was initially (it could have changed as part of being
// in a loop or similar). So, we emit a backtracking section that
// pushes/pops the starting position before falling through.
if (isInLoop)
{
// If we're in a loop, different iterations of the loop need their own
// starting position, so push it on to the stack. If we're not in a loop,
// the local will maintain its value and will suffice.
// if (stackpos + 1 >= base.runstack.Length) Array.Resize(ref base.runstack, base.runstack.Length * 2);
// base.runstack[stackpos++] = startingPos;
EmitStackResizeIfNeeded(1);
EmitStackPush(() => Ldloc(startingPos));
}
// Skip past the backtracking section
// goto backtrackingEnd;
Label backtrackingEnd = DefineLabel();
Br(backtrackingEnd);
// Emit a backtracking section that restores the capture's state and then jumps to the previous done label
Label backtrack = DefineLabel();
MarkLabel(backtrack);
if (isInLoop)
{
EmitStackPop();
Stloc(startingPos);
}
// goto doneLabel;
BrFar(doneLabel);
doneLabel = backtrack;
MarkLabel(backtrackingEnd);
}
}
// Emits code to unwind the capture stack until the crawl position specified in the provided local.
void EmitUncaptureUntil(LocalBuilder startingCapturePos)
{
Debug.Assert(startingCapturePos != null);
// while (base.Crawlpos() > startingCapturePos) base.Uncapture();
Label condition = DefineLabel();
Label body = DefineLabel();
Br(condition);
MarkLabel(body);
Ldthis();
Call(UncaptureMethod);
MarkLabel(condition);
Ldthis();
Call(CrawlposMethod);
Ldloc(startingCapturePos);
Bgt(body);
}
// Emits the code to handle a positive lookaround assertion. This is a positive lookahead
// for left-to-right and a positive lookbehind for right-to-left.
void EmitPositiveLookaroundAssertion(RegexNode node)
{
Debug.Assert(node.Kind is RegexNodeKind.PositiveLookaround, $"Unexpected type: {node.Kind}");
Debug.Assert(node.ChildCount() == 1, $"Expected 1 child, found {node.ChildCount()}");
if (analysis.HasRightToLeft)
{
// Lookarounds are the only places in the node tree where we might change direction,
// i.e. where we might go from RegexOptions.None to RegexOptions.RightToLeft, or vice
// versa. This is because lookbehinds are implemented by making the whole subgraph be
// RegexOptions.RightToLeft and reversed. Since we use static position to optimize left-to-right
// and don't use it in support of right-to-left, we need to resync the static position
// to the current position when entering a lookaround, just in case we're changing direction.
TransferSliceStaticPosToPos(forceSliceReload: true);
}
// Save off pos. We'll need to reset this upon successful completion of the lookaround.
// startingPos = pos;
LocalBuilder startingPos = DeclareInt32();
Ldloc(pos);
Stloc(startingPos);
int startingTextSpanPos = sliceStaticPos;
// Check for timeout. Lookarounds result in re-processing the same input, so while not
// technically backtracking, it's appropriate to have a timeout check.
EmitTimeoutCheckIfNeeded();
// Emit the child.
RegexNode child = node.Child(0);
if (analysis.MayBacktrack(child))
{
// Lookarounds are implicitly atomic, so we need to emit the node as atomic if it might backtrack.
EmitAtomic(node, null);
}
else
{
EmitNode(child);
}
// After the child completes successfully, reset the text positions.
// Do not reset captures, which persist beyond the lookaround.
// pos = startingPos;
// slice = inputSpan.Slice(pos);
Ldloc(startingPos);
Stloc(pos);
SliceInputSpan();
sliceStaticPos = startingTextSpanPos;
}
// Emits the code to handle a negative lookaround assertion. This is a negative lookahead
// for left-to-right and a negative lookbehind for right-to-left.
void EmitNegativeLookaroundAssertion(RegexNode node)
{
Debug.Assert(node.Kind is RegexNodeKind.NegativeLookaround, $"Unexpected type: {node.Kind}");
Debug.Assert(node.ChildCount() == 1, $"Expected 1 child, found {node.ChildCount()}");
if (analysis.HasRightToLeft)
{
// Lookarounds are the only places in the node tree where we might change direction,
// i.e. where we might go from RegexOptions.None to RegexOptions.RightToLeft, or vice
// versa. This is because lookbehinds are implemented by making the whole subgraph be
// RegexOptions.RightToLeft and reversed. Since we use static position to optimize left-to-right
// and don't use it in support of right-to-left, we need to resync the static position
// to the current position when entering a lookaround, just in case we're changing direction.
TransferSliceStaticPosToPos(forceSliceReload: true);
}
Label originalDoneLabel = doneLabel;
// Save off pos. We'll need to reset this upon successful completion of the lookaround.
// startingPos = pos;
LocalBuilder startingPos = DeclareInt32();
Ldloc(pos);
Stloc(startingPos);
int startingTextSpanPos = sliceStaticPos;
Label negativeLookaheadDoneLabel = DefineLabel();
doneLabel = negativeLookaheadDoneLabel;
// Check for timeout. Lookarounds result in re-processing the same input, so while not
// technically backtracking, it's appropriate to have a timeout check.
EmitTimeoutCheckIfNeeded();
RegexNode child = node.Child(0);
// Ensure we're able to uncapture anything captured by the child.
// Note that this differs ever so slightly from the source generator. The source
// generator only defines a local for capturePos if not in a loop (as it calls to a helper
// method where the argument acts implicitly as a local), but the compiler
// needs to store the popped stack value somewhere so that it can repeatedly compare
// that value against Crawlpos, so capturePos is always declared if there are captures.
bool isInLoop = false;
LocalBuilder? capturePos = analysis.MayContainCapture(child) ? DeclareInt32() : null;
if (capturePos is not null)
{
// If we're inside a loop, push the current crawl position onto the stack,
// so that each iteration tracks its own value. Otherwise, store it into a local.
isInLoop = analysis.IsInLoop(node);
if (isInLoop)
{
EmitStackResizeIfNeeded(1);
EmitStackPush(() =>
{
// base.Crawlpos();
Ldthis();
Call(CrawlposMethod);
});
}
else
{
// capturePos = base.Crawlpos();
Ldthis();
Call(CrawlposMethod);
Stloc(capturePos);
}
}
// Emit the child.
if (analysis.MayBacktrack(child))
{
// Lookarounds are implicitly atomic, so we need to emit the node as atomic if it might backtrack.
EmitAtomic(node, null);
}
else
{
EmitNode(child);
}
// If the generated code ends up here, it matched the lookaround, which actually
// means failure for a _negative_ lookaround, so we need to jump to the original done.
// goto originalDoneLabel;
if (capturePos is not null && isInLoop)
{
// Pop the crawl position from the stack.
// stackpos--;
Ldloc(stackpos);
Ldc(1);
Sub();
Stloc(stackpos);
}
BrFar(originalDoneLabel);
// Failures (success for a negative lookaround) jump here.
MarkLabel(negativeLookaheadDoneLabel);
if (doneLabel == negativeLookaheadDoneLabel)
{
doneLabel = originalDoneLabel;
}
// After the child completes in failure (success for negative lookaround), reset the text positions.
// pos = startingPos;
Ldloc(startingPos);
Stloc(pos);
SliceInputSpan();
sliceStaticPos = startingTextSpanPos;
// And uncapture anything if necessary. Negative lookaround captures don't persist beyond the lookaround.
if (capturePos is not null)
{
if (isInLoop)
{
// capturepos = base.runstack[--stackpos];
EmitStackPop();
Stloc(capturePos);
}
// while (base.Crawlpos() > capturepos) base.Uncapture();
EmitUncaptureUntil(capturePos);
}
doneLabel = originalDoneLabel;
}
// Emits the code for the node.
void EmitNode(RegexNode node, RegexNode? subsequent = null, bool emitLengthChecksIfRequired = true)
{
// Before we handle general-purpose matching logic for nodes, handle any special-casing.
if (_regexTree!.FindOptimizations.FindMode == FindNextStartingPositionMode.LiteralAfterLoop_LeftToRight &&
_regexTree!.FindOptimizations.LiteralAfterLoop?.LoopNode == node)
{
// This is the set loop that's part of the literal-after-loop optimization: the end of the loop
// is stored in runtrackpos, so we just need to transfer that to pos. The optimization is only
// selected if the shape of the tree is amenable.
Debug.Assert(sliceStaticPos == 0, "This should be the first node and thus static position shouldn't have advanced.");
// pos = base.runtrackpos;
Mvfldloc(RuntrackposField, pos);
SliceInputSpan();
return;
}
if (!StackHelper.TryEnsureSufficientExecutionStack())
{
StackHelper.CallOnEmptyStack(EmitNode, node, subsequent, emitLengthChecksIfRequired);
return;
}
// RightToLeft doesn't take advantage of static positions. While RightToLeft won't update static
// positions, a previous operation may have left us with a non-zero one. Make sure it's zero'd out
// such that pos and slice are up-to-date. Note that RightToLeft also shouldn't use the slice span,
// as it's not kept up-to-date; any RightToLeft implementation that wants to use it must first update
// it from pos.
if ((node.Options & RegexOptions.RightToLeft) != 0)
{
TransferSliceStaticPosToPos();
}
switch (node.Kind)
{
case RegexNodeKind.Beginning:
case RegexNodeKind.Start:
case RegexNodeKind.Bol:
case RegexNodeKind.Eol:
case RegexNodeKind.End:
case RegexNodeKind.EndZ:
EmitAnchors(node);
return;
case RegexNodeKind.Boundary:
case RegexNodeKind.NonBoundary:
case RegexNodeKind.ECMABoundary:
case RegexNodeKind.NonECMABoundary:
EmitBoundary(node);
return;
case RegexNodeKind.Multi:
EmitMultiChar(node, emitLengthChecksIfRequired);
return;
case RegexNodeKind.One:
case RegexNodeKind.Notone:
case RegexNodeKind.Set:
EmitSingleChar(node, emitLengthChecksIfRequired);
return;
case RegexNodeKind.Oneloop:
case RegexNodeKind.Notoneloop:
case RegexNodeKind.Setloop:
EmitSingleCharLoop(node, subsequent, emitLengthChecksIfRequired);
return;
case RegexNodeKind.Onelazy:
case RegexNodeKind.Notonelazy:
case RegexNodeKind.Setlazy:
EmitSingleCharLazy(node, subsequent, emitLengthChecksIfRequired);
return;
case RegexNodeKind.Oneloopatomic:
case RegexNodeKind.Notoneloopatomic:
case RegexNodeKind.Setloopatomic:
EmitSingleCharAtomicLoop(node);
return;
case RegexNodeKind.Loop:
EmitLoop(node);
return;
case RegexNodeKind.Lazyloop:
EmitLazy(node);
return;
case RegexNodeKind.Alternate:
EmitAlternation(node);
return;
case RegexNodeKind.Concatenate:
EmitConcatenation(node, subsequent, emitLengthChecksIfRequired);
return;
case RegexNodeKind.Atomic:
EmitAtomic(node, subsequent);
return;
case RegexNodeKind.Backreference:
EmitBackreference(node);
return;
case RegexNodeKind.BackreferenceConditional:
EmitBackreferenceConditional(node);
return;
case RegexNodeKind.ExpressionConditional:
EmitExpressionConditional(node);
return;
case RegexNodeKind.Capture:
EmitCapture(node, subsequent);
return;
case RegexNodeKind.PositiveLookaround:
EmitPositiveLookaroundAssertion(node);
return;
case RegexNodeKind.NegativeLookaround:
EmitNegativeLookaroundAssertion(node);
return;
case RegexNodeKind.Nothing:
BrFar(doneLabel);
return;
case RegexNodeKind.Empty:
// Emit nothing.
return;
case RegexNodeKind.UpdateBumpalong:
EmitUpdateBumpalong(node);
return;
}
// All nodes should have been handled.
Debug.Fail($"Unexpected node type: {node.Kind}");
}
// Emits the node for an atomic.
void EmitAtomic(RegexNode node, RegexNode? subsequent)
{
Debug.Assert(node.Kind is RegexNodeKind.Atomic or RegexNodeKind.PositiveLookaround or RegexNodeKind.NegativeLookaround or RegexNodeKind.ExpressionConditional, $"Unexpected type: {node.Kind}");
Debug.Assert(node.Kind is RegexNodeKind.ExpressionConditional ? node.ChildCount() >= 1 : node.ChildCount() == 1, $"Unexpected number of children: {node.ChildCount()}");
RegexNode child = node.Child(0);
if (!analysis.MayBacktrack(child))
{
// If the child has no backtracking, the atomic is a nop and we can just skip it.
// Note that the source generator equivalent for this is in the top-level EmitNode, in order to avoid
// outputting some extra comments and scopes. As such formatting isn't a concern for the compiler,
// the logic is instead here in EmitAtomic.
EmitNode(child, subsequent);
return;
}
// Grab the current done label and the current backtracking position. The purpose of the atomic node
// is to ensure that nodes after it that might backtrack skip over the atomic, which means after
// rendering the atomic's child, we need to reset the label so that subsequent backtracking doesn't
// see any label left set by the atomic's child. We also need to reset the backtracking stack position
// so that the state on the stack remains consistent.
Label originalDoneLabel = doneLabel;
// int startingStackpos = stackpos;
using RentedLocalBuilder startingStackpos = RentInt32Local();
Ldloc(stackpos);
Stloc(startingStackpos);
// Emit the child.
EmitNode(child, subsequent);
// Reset the stack position and done label.
// stackpos = startingStackpos;
Ldloc(startingStackpos);
Stloc(stackpos);
doneLabel = originalDoneLabel;
}
// Emits the code to handle updating base.runtextpos to pos in response to
// an UpdateBumpalong node. This is used when we want to inform the scan loop that
// it should bump from this location rather than from the original location.
void EmitUpdateBumpalong(RegexNode node)
{
Debug.Assert(node.Kind is RegexNodeKind.UpdateBumpalong, $"Unexpected type: {node.Kind}");
// if (base.runtextpos < pos)
// {
// base.runtextpos = pos;
// }
TransferSliceStaticPosToPos();
Ldthisfld(RuntextposField);
Ldloc(pos);
Label skipUpdate = DefineLabel();
Bge(skipUpdate);
Ldthis();
Ldloc(pos);
Stfld(RuntextposField);
MarkLabel(skipUpdate);
}
// Emits code for a concatenation
void EmitConcatenation(RegexNode node, RegexNode? subsequent, bool emitLengthChecksIfRequired)
{
Debug.Assert(node.Kind is RegexNodeKind.Concatenate, $"Unexpected type: {node.Kind}");
Debug.Assert(node.ChildCount() >= 2, $"Expected at least 2 children, found {node.ChildCount()}");
// Emit the code for each child one after the other.
int childCount = node.ChildCount();
for (int i = 0; i < childCount; i++)
{
// If we can find a subsequence of fixed-length children, we can emit a length check once for that sequence
// and then skip the individual length checks for each. We can also discover case-insensitive sequences that
// can be checked efficiently with methods like StartsWith.
if ((node.Options & RegexOptions.RightToLeft) == 0 &&
emitLengthChecksIfRequired &&
node.TryGetJoinableLengthCheckChildRange(i, out int requiredLength, out int exclusiveEnd))
{
EmitSpanLengthCheck(requiredLength);
for (; i < exclusiveEnd; i++)
{
if (node.TryGetOrdinalCaseInsensitiveString(i, exclusiveEnd, out int nodesConsumed, out string? caseInsensitiveString))
{
// if (!sliceSpan.Slice(sliceStaticPause).StartsWith(caseInsensitiveString, StringComparison.OrdinalIgnoreCase)) goto doneLabel;
if (sliceStaticPos > 0)
{
Ldloca(slice);
Ldc(sliceStaticPos);
Call(SpanSliceIntMethod);
}
else
{
Ldloc(slice);
}
Ldstr(caseInsensitiveString);
Call(StringAsSpanMethod);
Ldc((int)StringComparison.OrdinalIgnoreCase);
Call(SpanStartsWithSpanComparisonMethod);
BrfalseFar(doneLabel);
sliceStaticPos += caseInsensitiveString.Length;
i += nodesConsumed - 1;
continue;
}
EmitNode(node.Child(i), GetSubsequent(i, node, subsequent), emitLengthChecksIfRequired: false);
}
i--;
continue;
}
EmitNode(node.Child(i), GetSubsequent(i, node, subsequent));
}
// Gets the node to treat as the subsequent one to node.Child(index)
static RegexNode? GetSubsequent(int index, RegexNode node, RegexNode? subsequent)
{
int childCount = node.ChildCount();
for (int i = index + 1; i < childCount; i++)
{
RegexNode next = node.Child(i);
if (next.Kind is not RegexNodeKind.UpdateBumpalong) // skip node types that don't have a semantic impact
{
return next;
}
}
return subsequent;
}
}
// Emits the code to handle a single-character match.
void EmitSingleChar(RegexNode node, bool emitLengthCheck = true, LocalBuilder? offset = null)
{
Debug.Assert(node.IsOneFamily || node.IsNotoneFamily || node.IsSetFamily, $"Unexpected type: {node.Kind}");
bool rtl = (node.Options & RegexOptions.RightToLeft) != 0;
Debug.Assert(!rtl || offset is null);
if (emitLengthCheck)
{
if (!rtl)
{
// if ((uint)(sliceStaticPos + offset) >= slice.Length) goto Done;
EmitSpanLengthCheck(1, offset);
}
else
{
// if ((uint)(pos - 1) >= inputSpan.Length) goto Done;
Ldloc(pos);
Ldc(1);
Sub();
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
BgeUnFar(doneLabel);
}
}
if (!rtl)
{
// slice[staticPos + offset]
Ldloca(slice);
EmitSum(sliceStaticPos, offset);
}
else
{
// inputSpan[pos - 1]
Ldloca(inputSpan);
EmitSum(-1, pos);
}
Call(SpanGetItemMethod);
LdindU2();
// if (loadedChar != ch) goto doneLabel;
if (node.IsSetFamily)
{
EmitMatchCharacterClass(node.Str);
BrfalseFar(doneLabel);
}
else
{
Ldc(node.Ch);
if (node.IsOneFamily)
{
BneFar(doneLabel);
}
else // IsNotoneFamily
{
BeqFar(doneLabel);
}
}
if (!rtl)
{
sliceStaticPos++;
}
else
{
// pos--;
Ldloc(pos);
Ldc(1);
Sub();
Stloc(pos);
}
}
// Emits the code to handle a boundary check on a character.
void EmitBoundary(RegexNode node)
{
Debug.Assert(node.Kind is RegexNodeKind.Boundary or RegexNodeKind.NonBoundary or RegexNodeKind.ECMABoundary or RegexNodeKind.NonECMABoundary, $"Unexpected type: {node.Kind}");
if ((node.Options & RegexOptions.RightToLeft) != 0)
{
// RightToLeft doesn't use static position. This ensures it's 0.
TransferSliceStaticPosToPos();
}
// if (!IsBoundary(inputSpan, pos + sliceStaticPos)) goto doneLabel;
Ldloc(inputSpan);
Ldloc(pos);
if (sliceStaticPos > 0)
{
Ldc(sliceStaticPos);
Add();
}
switch (node.Kind)
{
case RegexNodeKind.Boundary:
Call(IsBoundaryMethod);
BrfalseFar(doneLabel);
break;
case RegexNodeKind.NonBoundary:
Call(IsBoundaryMethod);
BrtrueFar(doneLabel);
break;
case RegexNodeKind.ECMABoundary:
Call(IsECMABoundaryMethod);
BrfalseFar(doneLabel);
break;
default:
Debug.Assert(node.Kind == RegexNodeKind.NonECMABoundary);
Call(IsECMABoundaryMethod);
BrtrueFar(doneLabel);
break;
}
}
// Emits the code to handle various anchors.
void EmitAnchors(RegexNode node)
{
Debug.Assert(node.Kind is RegexNodeKind.Beginning or RegexNodeKind.Start or RegexNodeKind.Bol or RegexNodeKind.End or RegexNodeKind.EndZ or RegexNodeKind.Eol, $"Unexpected type: {node.Kind}");
Debug.Assert((node.Options & RegexOptions.RightToLeft) == 0 || sliceStaticPos == 0);
Debug.Assert(sliceStaticPos >= 0);
Debug.Assert(sliceStaticPos >= 0);
switch (node.Kind)
{
case RegexNodeKind.Beginning:
case RegexNodeKind.Start:
if (sliceStaticPos > 0)
{
// If we statically know we've already matched part of the regex, there's no way we're at the
// beginning or start, as we've already progressed past it.
BrFar(doneLabel);
}
else
{
// if (pos > 0/start) goto doneLabel;
Ldloc(pos);
if (node.Kind == RegexNodeKind.Beginning)
{
Ldc(0);
}
else
{
Ldthisfld(RuntextstartField);
}
BneFar(doneLabel);
}
break;
case RegexNodeKind.Bol:
if (sliceStaticPos > 0)
{
// if (slice[sliceStaticPos - 1] != '\n') goto doneLabel;
Ldloca(slice);
Ldc(sliceStaticPos - 1);
Call(SpanGetItemMethod);
LdindU2();
Ldc('\n');
BneFar(doneLabel);
}
else
{
// We can't use our slice in this case, because we'd need to access slice[-1], so we access the inputSpan directly:
// if (pos > 0 && inputSpan[pos - 1] != '\n') goto doneLabel;
Label success = DefineLabel();
Ldloc(pos);
Ldc(0);
Ble(success);
Ldloca(inputSpan);
Ldloc(pos);
Ldc(1);
Sub();
Call(SpanGetItemMethod);
LdindU2();
Ldc('\n');
BneFar(doneLabel);
MarkLabel(success);
}
break;
case RegexNodeKind.End:
if (sliceStaticPos > 0)
{
// if (sliceStaticPos < slice.Length) goto doneLabel;
Ldc(sliceStaticPos);
Ldloca(slice);
}
else
{
// if (pos < inputSpan.Length) goto doneLabel;
Ldloc(pos);
Ldloca(inputSpan);
}
Call(SpanGetLengthMethod);
BltUnFar(doneLabel);
break;
case RegexNodeKind.EndZ:
if (sliceStaticPos > 0)
{
// if (sliceStaticPos < slice.Length - 1) goto doneLabel;
Ldc(sliceStaticPos);
Ldloca(slice);
}
else
{
// if (pos < inputSpan.Length - 1) goto doneLabel
Ldloc(pos);
Ldloca(inputSpan);
}
Call(SpanGetLengthMethod);
Ldc(1);
Sub();
BltFar(doneLabel);
goto case RegexNodeKind.Eol;
case RegexNodeKind.Eol:
if (sliceStaticPos > 0)
{
// if (sliceStaticPos < slice.Length && slice[sliceStaticPos] != '\n') goto doneLabel;
Label success = DefineLabel();
Ldc(sliceStaticPos);
Ldloca(slice);
Call(SpanGetLengthMethod);
BgeUn(success);
Ldloca(slice);
Ldc(sliceStaticPos);
Call(SpanGetItemMethod);
LdindU2();
Ldc('\n');
BneFar(doneLabel);
MarkLabel(success);
}
else
{
// if ((uint)pos < (uint)inputSpan.Length && inputSpan[pos] != '\n') goto doneLabel;
Label success = DefineLabel();
Ldloc(pos);
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
BgeUn(success);
Ldloca(inputSpan);
Ldloc(pos);
Call(SpanGetItemMethod);
LdindU2();
Ldc('\n');
BneFar(doneLabel);
MarkLabel(success);
}
break;
}
}
// Emits the code to handle a multiple-character match.
void EmitMultiChar(RegexNode node, bool emitLengthCheck)
{
Debug.Assert(node.Kind is RegexNodeKind.Multi, $"Unexpected type: {node.Kind}");
EmitMultiCharString(node.Str!, emitLengthCheck, (node.Options & RegexOptions.RightToLeft) != 0);
}
void EmitMultiCharString(string str, bool emitLengthCheck, bool rightToLeft)
{
Debug.Assert(str.Length >= 2);
if (rightToLeft)
{
Debug.Assert(emitLengthCheck);
TransferSliceStaticPosToPos();
// if ((uint)(pos - str.Length) >= inputSpan.Length) goto doneLabel;
Ldloc(pos);
Ldc(str.Length);
Sub();
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
BgeUnFar(doneLabel);
for (int i = str.Length - 1; i >= 0; i--)
{
// if (inputSpan[--pos] != str[str.Length - 1 - i]) goto doneLabel
Ldloc(pos);
Ldc(1);
Sub();
Stloc(pos);
Ldloca(inputSpan);
Ldloc(pos);
Call(SpanGetItemMethod);
LdindU2();
Ldc(str[i]);
BneFar(doneLabel);
}
return;
}
Ldloca(slice);
Ldc(sliceStaticPos);
Call(SpanSliceIntMethod);
Ldstr(str);
Call(StringAsSpanMethod);
Call(SpanStartsWithSpanMethod);
BrfalseFar(doneLabel);
sliceStaticPos += str.Length;
}
// Emits the code to handle a backtracking, single-character loop.
void EmitSingleCharLoop(RegexNode node, RegexNode? subsequent = null, bool emitLengthChecksIfRequired = true)
{
Debug.Assert(node.Kind is RegexNodeKind.Oneloop or RegexNodeKind.Notoneloop or RegexNodeKind.Setloop, $"Unexpected type: {node.Kind}");
// If this is actually atomic based on its parent, emit it as atomic instead; no backtracking necessary.
if (analysis.IsAtomicByAncestor(node))
{
EmitSingleCharAtomicLoop(node);
return;
}
// If this is actually a repeater, emit that instead; no backtracking necessary.
if (node.M == node.N)
{
EmitSingleCharRepeater(node, emitLengthChecksIfRequired);
return;
}
// Emit backtracking around an atomic single char loop. We can then implement the backtracking
// as an afterthought, since we know exactly how many characters are accepted by each iteration
// of the wrapped loop (1) and that there's nothing captured by the loop.
Debug.Assert(node.M < node.N);
Label backtrackingLabel = DefineLabel();
Label endLoop = DefineLabel();
LocalBuilder startingPos = DeclareInt32();
LocalBuilder endingPos = DeclareInt32();
LocalBuilder? capturePos = expressionHasCaptures ? DeclareInt32() : null;
bool rtl = (node.Options & RegexOptions.RightToLeft) != 0;
bool isInLoop = analysis.IsInLoop(node);
// We're about to enter a loop, so ensure our text position is 0.
TransferSliceStaticPosToPos();
// Grab the current position, then emit the loop as atomic, and then
// grab the current position again. Even though we emit the loop without
// knowledge of backtracking, we can layer it on top by just walking back
// through the individual characters (a benefit of the loop matching exactly
// one character per iteration, no possible captures within the loop, etc.)
// int startingPos = pos;
Ldloc(pos);
Stloc(startingPos);
EmitSingleCharAtomicLoop(node);
// int endingPos = pos;
TransferSliceStaticPosToPos();
Ldloc(pos);
Stloc(endingPos);
// startingPos += node.M; // or -= for rtl
if (node.M > 0)
{
Ldloc(startingPos);
Ldc(!rtl ? node.M : -node.M);
Add();
Stloc(startingPos);
}
// goto endLoop;
BrFar(endLoop);
// Backtracking section. Subsequent failures will jump to here, at which
// point we decrement the matched count as long as it's above the minimum
// required, and try again by flowing to everything that comes after this.
MarkLabel(backtrackingLabel);
if (isInLoop)
{
// This loop is inside of another loop, which means we persist state
// on the backtracking stack rather than relying on locals to always
// hold the right state (if we didn't do that, another iteration of the
// outer loop could have resulted in the locals being overwritten).
// Pop the relevant state from the stack.
if (capturePos is not null)
{
// Note that this differs ever so slightly from the source generator. The source
// generator only defines a local for capturePos if not in a loop, but the compiler
// needs to store the popped stack value somewhere so that it can repeatedly compare
// that value against Crawlpos, so capturePos is always declared if there are captures.
// capturepos = base.runstack[--stackpos];
// while (base.Crawlpos() > capturepos) base.Uncapture();
EmitStackPop();
Stloc(capturePos);
EmitUncaptureUntil(capturePos);
}
// endingPos = base.runstack[--stackpos];
// startingPos = base.runstack[--stackpos];
EmitStackPop();
Stloc(endingPos);
EmitStackPop();
Stloc(startingPos);
}
else if (capturePos is not null)
{
// Since we're not in a loop, we're using a local to track the crawl position.
// Unwind back to the position we were at prior to running the code after this loop.
EmitUncaptureUntil(capturePos);
}
// We're backtracking. Check the timeout.
EmitTimeoutCheckIfNeeded();
// if (startingPos >= endingPos) goto doneLabel; // or <= for rtl
Ldloc(startingPos);
Ldloc(endingPos);
if (!rtl)
{
BgeFar(doneLabel);
}
else
{
BleFar(doneLabel);
}
if (!rtl &&
node.N > 1 &&
subsequent?.FindStartingLiteralNode() is RegexNode literal &&
CanEmitIndexOf(literal, out int literalLength))
{
// endingPos = inputSpan.Slice(startingPos, Math.Min(inputSpan.Length, endingPos + literal.Length - 1) - startingPos).LastIndexOf(literal);
// if (endingPos < 0)
// {
// goto doneLabel;
// }
Ldloca(inputSpan);
Ldloc(startingPos);
if (literalLength > 1)
{
// Math.Min(inputSpan.Length, endingPos + literal.Length - 1) - startingPos
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
Ldloc(endingPos);
Ldc(literalLength - 1);
Add();
Call(MathMinIntIntMethod);
}
else
{
// endingPos - startingPos
Ldloc(endingPos);
}
Ldloc(startingPos);
Sub();
Call(SpanSliceIntIntMethod);
EmitIndexOf(literal, useLast: true, negate: false);
Stloc(endingPos);
Ldloc(endingPos);
Ldc(0);
BltFar(doneLabel);
// endingPos += startingPos;
Ldloc(endingPos);
Ldloc(startingPos);
Add();
Stloc(endingPos);
}
else
{
// endingPos--; // or ++ for rtl
Ldloc(endingPos);
Ldc(!rtl ? 1 : -1);
Sub();
Stloc(endingPos);
}
// pos = endingPos;
Ldloc(endingPos);
Stloc(pos);
if (!rtl)
{
// slice = inputSpan.Slice(pos);
SliceInputSpan();
}
MarkLabel(endLoop);
if (isInLoop)
{
// We're in a loop and thus can't rely on locals correctly holding the state we
// need (the locals could be overwritten by a subsequent iteration). Push the state
// on to the backtracking stack.
EmitStackResizeIfNeeded(2 + (capturePos is not null ? 1 : 0));
EmitStackPush(() => Ldloc(startingPos));
EmitStackPush(() => Ldloc(endingPos));
if (capturePos is not null)
{
EmitStackPush(() =>
{
// base.Crawlpos();
Ldthis();
Call(CrawlposMethod);
});
}
}
else if (capturePos is not null)
{
// We're not in a loop and so can trust our locals. Store the current capture position
// into the capture position local; we'll uncapture back to this when backtracking to
// remove any captures from after this loop that we need to throw away.
// capturePos = base.Crawlpos();
Ldthis();
Call(CrawlposMethod);
Stloc(capturePos);
}
doneLabel = backtrackingLabel; // leave set to the backtracking label for all subsequent nodes
}
void EmitSingleCharLazy(RegexNode node, RegexNode? subsequent = null, bool emitLengthChecksIfRequired = true)
{
Debug.Assert(node.Kind is RegexNodeKind.Onelazy or RegexNodeKind.Notonelazy or RegexNodeKind.Setlazy, $"Unexpected type: {node.Kind}");
// Emit the min iterations as a repeater. Any failures here don't necessitate backtracking,
// as the lazy itself failed to match, and there's no backtracking possible by the individual
// characters/iterations themselves.
if (node.M > 0)
{
EmitSingleCharRepeater(node, emitLengthChecksIfRequired);
}
// If the whole thing was actually that repeater, we're done. Similarly, if this is actually an atomic
// lazy loop, nothing will ever backtrack into this node, so we never need to iterate more than the minimum.
if (node.M == node.N || analysis.IsAtomicByAncestor(node))
{
return;
}
Debug.Assert(node.M < node.N);
bool rtl = (node.Options & RegexOptions.RightToLeft) != 0;
// We now need to match one character at a time, each time allowing the remainder of the expression
// to try to match, and only matching another character if the subsequent expression fails to match.
// We're about to enter a loop, so ensure our text position is 0.
TransferSliceStaticPosToPos();
// If the loop isn't unbounded, track the number of iterations and the max number to allow.
LocalBuilder? iterationCount = null;
int? maxIterations = null;
if (node.N != int.MaxValue)
{
maxIterations = node.N - node.M;
// int iterationCount = 0;
iterationCount = DeclareInt32();
Ldc(0);
Stloc(iterationCount);
}
// Track the current crawl position. Upon backtracking, we'll unwind any captures beyond this point.
LocalBuilder? capturepos = expressionHasCaptures ? DeclareInt32() : null;
// Track the current pos. Each time we backtrack, we'll reset to the stored position, which
// is also incremented each time we match another character in the loop.
// int startingPos = pos;
LocalBuilder startingPos = DeclareInt32();
Ldloc(pos);
Stloc(startingPos);
// Skip the backtracking section for the initial subsequent matching. We've already matched the
// minimum number of iterations, which means we can successfully match with zero additional iterations.
// goto endLoopLabel;
Label endLoopLabel = DefineLabel();
BrFar(endLoopLabel);
// Backtracking section. Subsequent failures will jump to here.
Label backtrackingLabel = DefineLabel();
MarkLabel(backtrackingLabel);
// Uncapture any captures if the expression has any. It's possible the captures it has
// are before this node, in which case this is wasted effort, but still functionally correct.
if (capturepos is not null)
{
// while (base.Crawlpos() > capturepos) base.Uncapture();
EmitUncaptureUntil(capturepos);
}
// If there's a max number of iterations, see if we've exceeded the maximum number of characters
// to match. If we haven't, increment the iteration count.
if (maxIterations is not null)
{
// if (iterationCount >= maxIterations) goto doneLabel;
Ldloc(iterationCount!);
Ldc(maxIterations.Value);
BgeFar(doneLabel);
// iterationCount++;
Ldloc(iterationCount!);
Ldc(1);
Add();
Stloc(iterationCount!);
}
// We're backtracking. Check the timeout.
EmitTimeoutCheckIfNeeded();
// Now match the next item in the lazy loop. We need to reset the pos to the position
// just after the last character in this loop was matched, and we need to store the resulting position
// for the next time we backtrack.
// pos = startingPos;
// Match single char;
Ldloc(startingPos);
Stloc(pos);
SliceInputSpan();
EmitSingleChar(node);
TransferSliceStaticPosToPos();
// Now that we've appropriately advanced by one character and are set for what comes after the loop,
// see if we can skip ahead more iterations by doing a search for a following literal.
if (!rtl &&
iterationCount is null &&
node.Kind is RegexNodeKind.Notonelazy &&
subsequent?.FindStartingLiteral() is RegexNode.StartingLiteralData literal &&
!literal.Negated && // not negated; can't search for both the node.Ch and a negated subsequent char with an IndexOf* method
(literal.String is not null ||
literal.SetChars is not null ||
literal.Range.LowInclusive == literal.Range.HighInclusive ||
(literal.Range.LowInclusive <= node.Ch && node.Ch <= literal.Range.HighInclusive))) // for ranges, only allow when the range overlaps with the target, since there's no accelerated way to search for the union
{
// e.g. "<[^>]*?>"
// Whether the not'd character matches the subsequent literal. This impacts whether we need to search
// for both or just the literal, as well as what assumptions we can make once a match is found.
bool overlap;
// This lazy loop will consume all characters other than node.Ch until the subsequent literal.
// We can implement it to search for either that char or the literal, whichever comes first.
Ldloc(slice);
if (literal.String is not null) // string literal
{
overlap = literal.String[0] == node.Ch;
if (overlap)
{
// startingPos = slice.IndexOf(node.Ch);
Ldc(node.Ch);
Call(SpanIndexOfCharMethod);
}
else
{
// startingPos = slice.IndexOfAny(node.Ch, literal.String[0]);
Ldc(node.Ch);
Ldc(literal.String[0]);
Call(SpanIndexOfAnyCharCharMethod);
}
}
else if (literal.SetChars is not null) // set literal
{
overlap = literal.SetChars.Contains(node.Ch);
switch ((overlap, literal.SetChars.Length))
{
case (true, 2):
// startingPos = slice.IndexOfAny(literal.SetChars[0], literal.SetChars[1]);
Ldc(literal.SetChars[0]);
Ldc(literal.SetChars[1]);
Call(SpanIndexOfAnyCharCharMethod);
break;
case (true, 3):
// startingPos = slice.IndexOfAny(literal.SetChars[0], literal.SetChars[1], literal.SetChars[2]);
Ldc(literal.SetChars[0]);
Ldc(literal.SetChars[1]);
Ldc(literal.SetChars[2]);
Call(SpanIndexOfAnyCharCharCharMethod);
break;
case (true, _):
// startingPos = slice.IndexOfAny(literal.SetChars);
EmitIndexOfAnyWithSearchValuesOrLiteral(literal.SetChars);
break;
case (false, 2):
// startingPos = slice.IndexOfAny(node.Ch, literal.SetChars[0], literal.SetChars[1]);
Ldc(node.Ch);
Ldc(literal.SetChars[0]);
Ldc(literal.SetChars[1]);
Call(SpanIndexOfAnyCharCharCharMethod);
break;
case (false, _):
// startingPos = slice.IndexOfAny($"{node.Ch}{literal.SetChars}");
EmitIndexOfAnyWithSearchValuesOrLiteral($"{node.Ch}{literal.SetChars}");
break;
}
}
else if (literal.Range.LowInclusive == literal.Range.HighInclusive) // single char from a RegexNode.One
{
overlap = literal.Range.LowInclusive == node.Ch;
if (overlap)
{
// startingPos = slice.IndexOf(node.Ch);
Ldc(node.Ch);
Call(SpanIndexOfCharMethod);
}
else
{
// startingPos = slice.IndexOfAny(node.Ch, literal.Range.LowInclusive);
Ldc(node.Ch);
Ldc(literal.Range.LowInclusive);
Call(SpanIndexOfAnyCharCharMethod);
}
}
else // range literal
{
// startingPos = slice.IndexOfAnyInRange(literal.Range.LowInclusive, literal.Range.HighInclusive);
overlap = true;
Ldc(literal.Range.LowInclusive);
Ldc(literal.Range.HighInclusive);
Call(SpanIndexOfAnyInRangeMethod);
}
Stloc(startingPos);
// If the search didn't find anything, fail the match. If it did find something, then we need to consider whether
// that something is the loop character. If it's not, we've successfully backtracked to the next lazy location
// where we should evaluate the rest of the pattern. If it does match, then we need to consider whether there's
// overlap between the loop character and the literal. If there is overlap, this is also a place to check. But
// if there's not overlap, and if the found character is the loop character, we also want to fail the match here
// and now, as this means the loop ends before it gets to what needs to come after the loop, and thus the pattern
// can't possibly match here.
if (overlap)
{
// if (startingPos < 0) goto doneLabel;
Ldloc(startingPos);
Ldc(0);
BltFar(doneLabel);
}
else
{
// if ((uint)startingPos >= (uint)slice.Length) goto doneLabel;
Ldloc(startingPos);
Ldloca(slice);
Call(SpanGetLengthMethod);
BgeUnFar(doneLabel);
// if (slice[startingPos] == node.Ch) goto doneLabel;
Ldloca(slice);
Ldloc(startingPos);
Call(SpanGetItemMethod);
LdindU2();
Ldc(node.Ch);
BeqFar(doneLabel);
}
// pos += startingPos;
// slice = inputSpace.Slice(pos);
Ldloc(pos);
Ldloc(startingPos);
Add();
Stloc(pos);
SliceInputSpan();
}
else if (!rtl &&
iterationCount is null &&
node.Kind is RegexNodeKind.Setlazy &&
node.Str == RegexCharClass.AnyClass &&
subsequent?.FindStartingLiteralNode() is RegexNode literal2 &&
CanEmitIndexOf(literal2, out _))
{
// e.g. ".*?string" with RegexOptions.Singleline
// This lazy loop will consume all characters until the subsequent literal. If the subsequent literal
// isn't found, the loop fails. We can implement it to just search for that literal.
// startingPos = slice.IndexOf(literal);
Ldloc(slice);
EmitIndexOf(node, useLast: false, negate: false);
Stloc(startingPos);
// if (startingPos < 0) goto doneLabel;
Ldloc(startingPos);
Ldc(0);
BltFar(doneLabel);
// pos += startingPos;
// slice = inputSpace.Slice(pos);
Ldloc(pos);
Ldloc(startingPos);
Add();
Stloc(pos);
SliceInputSpan();
}
// Store the position we've left off at in case we need to iterate again.
// startingPos = pos;
Ldloc(pos);
Stloc(startingPos);
// Update the done label for everything that comes after this node. This is done after we emit the single char
// matching, as that failing indicates the loop itself has failed to match.
Label originalDoneLabel = doneLabel;
doneLabel = backtrackingLabel; // leave set to the backtracking label for all subsequent nodes
MarkLabel(endLoopLabel);
if (capturepos is not null)
{
// capturepos = base.CrawlPos();
Ldthis();
Call(CrawlposMethod);
Stloc(capturepos);
}
// If this loop is itself not in another loop, nothing more needs to be done:
// upon backtracking, locals being used by this loop will have retained their
// values and be up-to-date. But if this loop is inside another loop, multiple
// iterations of this loop each need their own state, so we need to use the stack
// to hold it, and we need a dedicated backtracking section to handle restoring
// that state before jumping back into the loop itself.
if (analysis.IsInLoop(node))
{
// Store the loop's state
// base.runstack[stackpos++] = startingPos;
// base.runstack[stackpos++] = capturepos;
// base.runstack[stackpos++] = iterationCount;
EmitStackResizeIfNeeded(1 + (capturepos is not null ? 1 : 0) + (iterationCount is not null ? 1 : 0));
EmitStackPush(() => Ldloc(startingPos));
if (capturepos is not null)
{
EmitStackPush(() => Ldloc(capturepos));
}
if (iterationCount is not null)
{
EmitStackPush(() => Ldloc(iterationCount));
}
// Skip past the backtracking section.
Label backtrackingEnd = DefineLabel();
BrFar(backtrackingEnd);
// Emit a backtracking section that restores the loop's state and then jumps to the previous done label.
Label backtrack = DefineLabel();
MarkLabel(backtrack);
// Restore the loop's state.
// iterationCount = base.runstack[--stackpos];
// capturepos = base.runstack[--stackpos];
// startingPos = base.runstack[--stackpos];
if (iterationCount is not null)
{
EmitStackPop();
Stloc(iterationCount);
}
if (capturepos is not null)
{
EmitStackPop();
Stloc(capturepos);
}
EmitStackPop();
Stloc(startingPos);
// goto doneLabel;
BrFar(doneLabel);
doneLabel = backtrack;
MarkLabel(backtrackingEnd);
}
}
void EmitLazy(RegexNode node)
{
Debug.Assert(node.Kind is RegexNodeKind.Lazyloop, $"Unexpected type: {node.Kind}");
Debug.Assert(node.M < int.MaxValue, $"Unexpected M={node.M}");
Debug.Assert(node.N >= node.M, $"Unexpected M={node.M}, N={node.N}");
Debug.Assert(node.ChildCount() == 1, $"Expected 1 child, found {node.ChildCount()}");
RegexNode child = node.Child(0);
int minIterations = node.M;
int maxIterations = node.N;
Label originalDoneLabel = doneLabel;
// If this is actually a repeater, reuse the loop implementation, as a loop and a lazy loop
// both need to greedily consume up to their min iteration count and are identical in
// behavior when min == max.
if (minIterations == maxIterations)
{
EmitLoop(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.
TransferSliceStaticPosToPos();
Label body = DefineLabel();
Label endLoop = DefineLabel();
// iterationCount = 0;
LocalBuilder iterationCount = DeclareInt32();
Ldc(0);
Stloc(iterationCount);
// startingPos = pos;
// sawEmpty = 0; // false
bool iterationMayBeEmpty = child.ComputeMinLength() == 0;
LocalBuilder? startingPos = null;
LocalBuilder? sawEmpty = null;
if (iterationMayBeEmpty)
{
startingPos = DeclareInt32();
Ldloc(pos);
Stloc(startingPos);
sawEmpty = DeclareInt32();
Ldc(0);
Stloc(sawEmpty);
}
// If the min count is 0, start out by jumping right to what's after the loop. Backtracking
// will then bring us back in to do further iterations.
if (minIterations == 0)
{
// goto endLoop;
BrFar(endLoop);
}
// Iteration body
MarkLabel(body);
// 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)
{
// 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); });
}
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);
// 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;
}
// 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);
}
// 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);
// 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);
// 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!);
}
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);
// 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 (iterationCount == 0) goto originalDoneLabel;
Ldloc(iterationCount);
Ldc(0);
BeqFar(originalDoneLabel);
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!);
}
// goto doneLabel;
BrFar(doneLabel);
}
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(() => Ldloc(startingPos!));
EmitStackPush(() => Ldloc(sawEmpty!));
}
}
if (expressionHasCaptures)
{
EmitStackPush(() => { Ldthis(); Call(CrawlposMethod); });
}
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);
// We're backtracking. Check the timeout.
EmitTimeoutCheckIfNeeded();
// int poppedCrawlPos = base.runstack[--stackpos];
// while (base.Crawlpos() > poppedCrawlPos) base.Uncapture();
EmitUncaptureUntilPopped();
if (isInLoop)
{
// 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);
}
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.
Label jumpToDone = DefineLabel();
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!);
Br(jumpToDone);
MarkLabel(sawEmptyZero);
}
if (maxIterations != int.MaxValue)
{
// if (iterationCount >= maxIterations) goto doneLabel;
Ldloc(iterationCount);
Ldc(maxIterations);
Bge(jumpToDone);
}
// goto body;
BrFar(body);
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);
}
// goto done;
BrFar(doneLabel);
doneLabel = backtrack;
MarkLabel(skipBacktrack);
}
}
// Emits the code to handle a loop (repeater) with a fixed number of iterations.
// RegexNode.M is used for the number of iterations (RegexNode.N is ignored), as this
// might be used to implement the required iterations of other kinds of loops.
void EmitSingleCharRepeater(RegexNode node, bool emitLengthChecksIfRequired = true)
{
Debug.Assert(node.IsOneFamily || node.IsNotoneFamily || node.IsSetFamily, $"Unexpected type: {node.Kind}");
int iterations = node.M;
bool rtl = (node.Options & RegexOptions.RightToLeft) != 0;
switch (iterations)
{
case 0:
// No iterations, nothing to do.
return;
case 1:
// Just match the individual item
EmitSingleChar(node, emitLengthChecksIfRequired);
return;
case <= RegexNode.MultiVsRepeaterLimit when node.IsOneFamily:
// This is a repeated case-sensitive character; emit it as a multi in order to get all the optimizations
// afforded to a multi, e.g. unrolling the loop with multi-char reads/comparisons at a time.
EmitMultiCharString(new string(node.Ch, iterations), emitLengthChecksIfRequired, rtl);
return;
}
if (rtl)
{
TransferSliceStaticPosToPos(); // we don't use static position with rtl
Label conditionLabel = DefineLabel();
Label bodyLabel = DefineLabel();
// for (int i = 0; ...)
using RentedLocalBuilder iterationLocal = RentInt32Local();
Ldc(0);
Stloc(iterationLocal);
BrFar(conditionLabel);
// TimeoutCheck();
// HandleSingleChar();
MarkLabel(bodyLabel);
EmitSingleChar(node);
// for (...; ...; i++)
Ldloc(iterationLocal);
Ldc(1);
Add();
Stloc(iterationLocal);
// for (...; i < iterations; ...)
MarkLabel(conditionLabel);
Ldloc(iterationLocal);
Ldc(iterations);
BltFar(bodyLabel);
return;
}
// if ((uint)(sliceStaticPos + iterations - 1) >= (uint)slice.Length) goto doneLabel;
if (emitLengthChecksIfRequired)
{
EmitSpanLengthCheck(iterations);
}
// If this is a repeater for anything,we only care about length and can jump past that length.
if (node.IsSetFamily && node.Str == RegexCharClass.AnyClass)
{
sliceStaticPos += iterations;
return;
}
// Arbitrary limit for unrolling vs creating a loop. We want to balance size in the generated
// code with other costs, like the (small) overhead of slicing to create the temp span to iterate.
const int MaxUnrollSize = 16;
if (iterations <= MaxUnrollSize)
{
// if (slice[sliceStaticPos] != c1 ||
// slice[sliceStaticPos + 1] != c2 ||
// ...)
// goto doneLabel;
for (int i = 0; i < iterations; i++)
{
EmitSingleChar(node, emitLengthCheck: false);
}
}
else
{
// ReadOnlySpan<char> tmp = slice.Slice(sliceStaticPos, iterations);
Ldloca(slice);
Ldc(sliceStaticPos);
Ldc(iterations);
Call(SpanSliceIntIntMethod);
// If we're able to vectorize the search, do so. Otherwise, fall back to a loop.
// For the loop, we're validating that each char matches the target node.
// For IndexOf, we're looking for the first thing that _doesn't_ match the target node,
// and thus similarly validating that everything does.
if (CanEmitIndexOf(node, out _))
{
// if (tmp.IndexOf(...) >= 0) goto doneLabel;
EmitIndexOf(node, useLast: false, negate: true);
Ldc(0);
BgeFar(doneLabel);
}
else
{
using RentedLocalBuilder spanLocal = RentReadOnlySpanCharLocal();
Stloc(spanLocal);
// for (int i = 0; i < tmp.Length; i++)
// {
// if (tmp[i] != ch) goto Done;
// }
Label conditionLabel = DefineLabel();
Label bodyLabel = DefineLabel();
using RentedLocalBuilder iterationLocal = RentInt32Local();
Ldc(0);
Stloc(iterationLocal);
BrFar(conditionLabel);
MarkLabel(bodyLabel);
LocalBuilder tmpTextSpanLocal = slice; // we want EmitSingleChar to refer to this temporary
int tmpTextSpanPos = sliceStaticPos;
slice = spanLocal;
sliceStaticPos = 0;
EmitSingleChar(node, emitLengthCheck: false, offset: iterationLocal);
slice = tmpTextSpanLocal;
sliceStaticPos = tmpTextSpanPos;
Ldloc(iterationLocal);
Ldc(1);
Add();
Stloc(iterationLocal);
MarkLabel(conditionLabel);
Ldloc(iterationLocal);
Ldloca(spanLocal);
Call(SpanGetLengthMethod);
BltFar(bodyLabel);
}
sliceStaticPos += iterations;
}
}
// Emits the code to handle a non-backtracking, variable-length loop around a single character comparison.
void EmitSingleCharAtomicLoop(RegexNode node)
{
Debug.Assert(node.Kind is RegexNodeKind.Oneloop or RegexNodeKind.Oneloopatomic or RegexNodeKind.Notoneloop or RegexNodeKind.Notoneloopatomic or RegexNodeKind.Setloop or RegexNodeKind.Setloopatomic, $"Unexpected type: {node.Kind}");
// If this is actually a repeater, emit that instead.
if (node.M == node.N)
{
EmitSingleCharRepeater(node);
return;
}
// If this is actually an optional single char, emit that instead.
if (node.M == 0 && node.N == 1)
{
EmitAtomicSingleCharZeroOrOne(node);
return;
}
Debug.Assert(node.N > node.M);
int minIterations = node.M;
int maxIterations = node.N;
bool rtl = (node.Options & RegexOptions.RightToLeft) != 0;
using RentedLocalBuilder iterationLocal = RentInt32Local();
Label atomicLoopDoneLabel = DefineLabel();
if (rtl)
{
TransferSliceStaticPosToPos(); // we don't use static position for rtl
Label conditionLabel = DefineLabel();
Label bodyLabel = DefineLabel();
// int i = 0;
Ldc(0);
Stloc(iterationLocal);
BrFar(conditionLabel);
// Body:
// TimeoutCheck();
MarkLabel(bodyLabel);
// if (pos <= iterationLocal) goto atomicLoopDoneLabel;
Ldloc(pos);
Ldloc(iterationLocal);
BleFar(atomicLoopDoneLabel);
// if (inputSpan[pos - i - 1] != ch) goto atomicLoopDoneLabel;
Ldloca(inputSpan);
Ldloc(pos);
Ldloc(iterationLocal);
Sub();
Ldc(1);
Sub();
Call(SpanGetItemMethod);
LdindU2();
if (node.IsSetFamily)
{
EmitMatchCharacterClass(node.Str);
BrfalseFar(atomicLoopDoneLabel);
}
else
{
Ldc(node.Ch);
if (node.IsOneFamily)
{
BneFar(atomicLoopDoneLabel);
}
else // IsNotoneFamily
{
BeqFar(atomicLoopDoneLabel);
}
}
// i++;
Ldloc(iterationLocal);
Ldc(1);
Add();
Stloc(iterationLocal);
// if (i >= maxIterations) goto atomicLoopDoneLabel;
MarkLabel(conditionLabel);
if (maxIterations != int.MaxValue)
{
Ldloc(iterationLocal);
Ldc(maxIterations);
BltFar(bodyLabel);
}
else
{
BrFar(bodyLabel);
}
}
else if (node.IsSetFamily && maxIterations == int.MaxValue && node.Str == RegexCharClass.AnyClass)
{
// .* was used with RegexOptions.Singleline, which means it'll consume everything. Just jump to the end.
// The unbounded constraint is the same as in the Notone case above, done purely for simplicity.
// int i = inputSpan.Length - pos;
TransferSliceStaticPosToPos();
Ldloca(inputSpan);
Call(SpanGetLengthMethod);
Ldloc(pos);
Sub();
Stloc(iterationLocal);
}
else if (maxIterations == int.MaxValue && CanEmitIndexOf(node, out _))
{
// We're unbounded and we can use an IndexOf method to perform the search. The unbounded restriction is
// purely for simplicity; it could be removed in the future with additional code to handle that case.
// int i = slice.Slice(sliceStaticPos).IndexOf(...);
if (sliceStaticPos > 0)
{
Ldloca(slice);
Ldc(sliceStaticPos);
Call(SpanSliceIntMethod);
}
else
{
Ldloc(slice);
}
EmitIndexOf(node, useLast: false, negate: true);
Stloc(iterationLocal);
// if (i >= 0) goto atomicLoopDoneLabel;
Ldloc(iterationLocal);
Ldc(0);
BgeFar(atomicLoopDoneLabel);
// i = slice.Length - sliceStaticPos;
Ldloca(slice);
Call(SpanGetLengthMethod);
if (sliceStaticPos > 0)
{
Ldc(sliceStaticPos);
Sub();
}
Stloc(iterationLocal);
}
else
{
// For everything else, do a normal loop.
// Transfer sliceStaticPos to pos to help with bounds check elimination on the loop.
TransferSliceStaticPosToPos();
Label conditionLabel = DefineLabel();
Label bodyLabel = DefineLabel();
// int i = 0;
Ldc(0);
Stloc(iterationLocal);
BrFar(conditionLabel);
// Body:
// TimeoutCheck();
MarkLabel(bodyLabel);
// if ((uint)i >= (uint)slice.Length) goto atomicLoopDoneLabel;
Ldloc(iterationLocal);
Ldloca(slice);
Call(SpanGetLengthMethod);
BgeUnFar(atomicLoopDoneLabel);
// if (slice[i] != ch) goto atomicLoopDoneLabel;
Ldloca(slice);
Ldloc(iterationLocal);
Call(SpanGetItemMethod);
LdindU2();
if (node.IsSetFamily)
{
EmitMatchCharacterClass(node.Str);
BrfalseFar(atomicLoopDoneLabel);
}
else
{
Ldc(node.Ch);
if (node.IsOneFamily)
{
BneFar(atomicLoopDoneLabel);
}
else // IsNotoneFamily
{
BeqFar(atomicLoopDoneLabel);
}
}
// i++;
Ldloc(iterationLocal);
Ldc(1);
Add();
Stloc(iterationLocal);
// if (i >= maxIterations) goto atomicLoopDoneLabel;
MarkLabel(conditionLabel);
if (maxIterations != int.MaxValue)
{
Ldloc(iterationLocal);
Ldc(maxIterations);
BltFar(bodyLabel);
}
else
{
BrFar(bodyLabel);
}
}
// Done:
MarkLabel(atomicLoopDoneLabel);
// Check to ensure we've found at least min iterations.
if (minIterations > 0)
{
Ldloc(iterationLocal);
Ldc(minIterations);
BltFar(doneLabel);
}
// Now that we've completed our optional iterations, advance the text span
// and pos by the number of iterations completed.
if (!rtl)
{
// slice = slice.Slice(i);
Ldloca(slice);
Ldloc(iterationLocal);
Call(SpanSliceIntMethod);
Stloc(slice);
// pos += i;
Ldloc(pos);
Ldloc(iterationLocal);
Add();
Stloc(pos);
}
else
{
// pos -= i;
Ldloc(pos);
Ldloc(iterationLocal);
Sub();
Stloc(pos);
}
}
// Emits the code to handle a non-backtracking optional zero-or-one loop.
void EmitAtomicSingleCharZeroOrOne(RegexNode node)
{
Debug.Assert(node.Kind is RegexNodeKind.Oneloop or RegexNodeKind.Oneloopatomic or RegexNodeKind.Notoneloop or RegexNodeKind.Notoneloopatomic or RegexNodeKind.Setloop or RegexNodeKind.Setloopatomic, $"Unexpected type: {node.Kind}");
Debug.Assert(node.M == 0 && node.N == 1);
bool rtl = (node.Options & RegexOptions.RightToLeft) != 0;
if (rtl)
{
TransferSliceStaticPosToPos(); // we don't use static pos for rtl
}
Label skipUpdatesLabel = DefineLabel();
if (!rtl)
{
// if ((uint)sliceStaticPos >= (uint)slice.Length) goto skipUpdatesLabel;
Ldc(sliceStaticPos);
Ldloca(slice);
Call(SpanGetLengthMethod);
BgeUnFar(skipUpdatesLabel);
}
else
{
// if (pos == 0) goto skipUpdatesLabel;
Ldloc(pos);
Ldc(0);
BeqFar(skipUpdatesLabel);
}
if (!rtl)
{
// if (slice[sliceStaticPos] != ch) goto skipUpdatesLabel;
Ldloca(slice);
Ldc(sliceStaticPos);
}
else
{
// if (inputSpan[pos - 1] != ch) goto skipUpdatesLabel;
Ldloca(inputSpan);
Ldloc(pos);
Ldc(1);
Sub();
}
Call(SpanGetItemMethod);
LdindU2();
if (node.IsSetFamily)
{
EmitMatchCharacterClass(node.Str);
BrfalseFar(skipUpdatesLabel);
}
else
{
Ldc(node.Ch);
if (node.IsOneFamily)
{
BneFar(skipUpdatesLabel);
}
else // IsNotoneFamily
{
BeqFar(skipUpdatesLabel);
}
}
if (!rtl)
{
// slice = slice.Slice(1);
Ldloca(slice);
Ldc(1);
Call(SpanSliceIntMethod);
Stloc(slice);
// pos++;
Ldloc(pos);
Ldc(1);
Add();
Stloc(pos);
}
else
{
// pos--;
Ldloc(pos);
Ldc(1);
Sub();
Stloc(pos);
}
MarkLabel(skipUpdatesLabel);
}
void EmitNonBacktrackingRepeater(RegexNode node)
{
Debug.Assert(node.Kind is RegexNodeKind.Loop or RegexNodeKind.Lazyloop, $"Unexpected type: {node.Kind}");
Debug.Assert(node.M < int.MaxValue, $"Unexpected M={node.M}");
Debug.Assert(node.M == node.N, $"Unexpected M={node.M} == N={node.N}");
Debug.Assert(node.ChildCount() == 1, $"Expected 1 child, found {node.ChildCount()}");
Debug.Assert(!analysis.MayBacktrack(node.Child(0)), $"Expected non-backtracking node {node.Kind}");
// Ensure every iteration of the loop sees a consistent value.
TransferSliceStaticPosToPos();
// Loop M==N times to match the child exactly that numbers of times.
Label condition = DefineLabel();
Label body = DefineLabel();
// for (int i = 0; ...)
using RentedLocalBuilder i = RentInt32Local();
Ldc(0);
Stloc(i);
BrFar(condition);
MarkLabel(body);
EmitNode(node.Child(0));
TransferSliceStaticPosToPos(); // make sure static the static position remains at 0 for subsequent constructs
// for (...; ...; i++)
Ldloc(i);
Ldc(1);
Add();
Stloc(i);
// for (...; i < node.M; ...)
MarkLabel(condition);
Ldloc(i);
Ldc(node.M);
BltFar(body);
}
void EmitLoop(RegexNode node)
{
Debug.Assert(node.Kind is RegexNodeKind.Loop or RegexNodeKind.Lazyloop, $"Unexpected type: {node.Kind}");
Debug.Assert(node.M < int.MaxValue, $"Unexpected M={node.M}");
Debug.Assert(node.N >= node.M, $"Unexpected M={node.M}, N={node.N}");
Debug.Assert(node.ChildCount() == 1, $"Expected 1 child, found {node.ChildCount()}");
RegexNode child = node.Child(0);
int minIterations = node.M;
int maxIterations = node.N;
// Special-case some repeaters.
if (minIterations == maxIterations)
{
switch (minIterations)
{
case 0:
// No iteration. Nop.
return;
case 1:
// One iteration. Just emit the child without any loop ceremony.
EmitNode(child);
return;
case > 1 when !analysis.MayBacktrack(child):
// The child doesn't backtrack. Emit it as a non-backtracking repeater.
// (If the child backtracks, we need to fall through to the more general logic
// that supports unwinding iterations.)
EmitNonBacktrackingRepeater(node);
return;
}
}
// 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.
TransferSliceStaticPosToPos();
bool isAtomic = analysis.IsAtomicByAncestor(node);
LocalBuilder? startingStackpos = null;
if (isAtomic || minIterations > 1)
{
// If the loop is atomic, constructs will need to backtrack around it, and as such any backtracking
// state pushed by the loop should be removed prior to exiting the loop. Similarly, if the loop has
// a minimum iteration count greater than 1, we might end up with at least one successful iteration
// only to find we can't iterate further, and will need to clear any pushed state from the backtracking
// stack. For both cases, we need to store the starting stack index so it can be reset to that position.
startingStackpos = DeclareInt32();
Ldloc(stackpos);
Stloc(startingStackpos);
}
Label originalDoneLabel = doneLabel;
Label body = DefineLabel();
Label endLoop = DefineLabel();
LocalBuilder iterationCount = DeclareInt32();
// Loops that match empty iterations need additional checks in place to prevent infinitely matching (since
// you could end up looping an infinite number of times at the same location). We can avoid those
// additional checks if we can prove that the loop can never match empty, which we can do by computing
// the minimum length of the child; only if it's 0 might iterations be empty.
bool iterationMayBeEmpty = child.ComputeMinLength() == 0;
LocalBuilder? startingPos = iterationMayBeEmpty ? DeclareInt32() : null;
// iterationCount = 0;
// startingPos = 0;
Ldc(0);
Stloc(iterationCount);
if (startingPos is not null)
{
Ldc(0);
Stloc(startingPos);
}
// Iteration body
MarkLabel(body);
// We need to store the starting pos and crawl position so that it may be backtracked through later.
// This needs to be the starting position from the iteration we're leaving, so it's pushed before updating
// it to pos. Note that unlike some other constructs that only need to push state on to the stack if
// they're inside of a loop (because if they're not inside of a loop, nothing would overwrite the locals),
// here we still need the stack, because each iteration of _this_ loop may have its own state, e.g. we
// need to know where each iteration began so when backtracking we can jump back to that location. This is
// true even if the loop is atomic, as we might need to backtrack within the loop in order to match the
// minimum iteration count.
EmitStackResizeIfNeeded(1 + (expressionHasCaptures ? 1 : 0) + (startingPos is not null ? 1 : 0));
if (expressionHasCaptures)
{
// base.runstack[stackpos++] = base.Crawlpos();
EmitStackPush(() => { Ldthis(); Call(CrawlposMethod); });
}
if (startingPos is not null)
{
EmitStackPush(() => Ldloc(startingPos));
}
EmitStackPush(() => Ldloc(pos));
// Save off some state. 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. Empty
// iterations are allowed as part of min matches, but once we've met the min quote, empty matches
// are considered match failures.
if (startingPos is not null)
{
// 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);
// 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
bool childBacktracks = doneLabel != iterationFailedLabel;
// Loop condition. Continue iterating greedily if we've not yet reached the maximum. We also need to stop
// iterating if the iteration matched empty and we already hit the minimum number of iterations. Otherwise,
// we've matched as many iterations as we can with this configuration. Jump to what comes after the loop.
switch ((minIterations > 0, maxIterations == int.MaxValue, iterationMayBeEmpty))
{
case (true, true, true):
// if (pos != startingPos || iterationCount < minIterations) goto body;
// goto endLoop;
Ldloc(pos);
Ldloc(startingPos!);
BneFar(body);
Ldloc(iterationCount);
Ldc(minIterations);
BltFar(body);
BrFar(endLoop);
break;
case (true, false, true):
// if ((pos != startingPos || iterationCount < minIterations) && iterationCount < maxIterations) goto body;
// goto endLoop;
Ldloc(iterationCount);
Ldc(maxIterations);
BgeFar(endLoop);
Ldloc(pos);
Ldloc(startingPos!);
BneFar(body);
Ldloc(iterationCount);
Ldc(minIterations);
BltFar(body);
BrFar(endLoop);
break;
case (false, true, true):
// if (pos != startingPos) goto body;
// goto endLoop;
Ldloc(pos);
Ldloc(startingPos!);
BneFar(body);
BrFar(endLoop);
break;
case (false, false, true):
// if (pos == startingPos || iterationCount >= maxIterations) goto endLoop;
// goto body;
Ldloc(pos);
Ldloc(startingPos!);
BeqFar(endLoop);
Ldloc(iterationCount);
Ldc(maxIterations);
BgeFar(endLoop);
BrFar(body);
break;
// Iterations won't be empty, but there is an upper bound. Whether or not there's a min iterations required, we need to keep
// iterating until we're at the maximum, and since the min is never more than the max, we don't need to check the min.
case (_, false, false):
// if (iterationCount >= maxIterations) goto endLoop;
// goto body;
Ldloc(iterationCount);
Ldc(maxIterations);
BgeFar(endLoop);
BrFar(body);
break;
// The loop has no upper bound and iterations can't be empty; regardless of whether there's a min iterations required,
// we need to loop again.
default:
// goto body;
BrFar(body);
break;
}
// Now handle what happens when an iteration fails, which could be an initial failure or it
// could be while backtracking. 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);
// iterationCount--;
Ldloc(iterationCount);
Ldc(1);
Sub();
Stloc(iterationCount);
// if (iterationCount < 0) goto originalDoneLabel;
Ldloc(iterationCount);
Ldc(0);
BltFar(originalDoneLabel);
// pos = base.runstack[--stackpos];
// slice = inputSpan.Slice(pos);
// startingPos = base.runstack[--stackpos];
// int poppedCrawlPos = base.runstack[--stackpos];
// while (base.Crawlpos() > poppedCrawlPos) base.Uncapture();
EmitStackPop();
Stloc(pos);
SliceInputSpan();
if (startingPos is not null)
{
EmitStackPop();
Stloc(startingPos);
}
EmitUncaptureUntilPopped();
// If there's a required minimum iteration count, validate now that we've processed enough iterations.
if (minIterations > 0)
{
if (childBacktracks)
{
// The child backtracks. If we don't have any iterations, there's nothing to backtrack into,
// and at least one iteration is required, so fail the loop.
// if (iterationCount == 0) goto originalDoneLabel;
Ldloc(iterationCount);
Ldc(0);
BeqFar(originalDoneLabel);
// We have at least one iteration; if that's insufficient to meet the minimum, backtrack
// into the previous iteration. We only need to do this check if the min iteration requirement
// is more than one, since the above check already handles the case where the min count is 1,
// since the only value that wouldn't meet that is 0.
if (minIterations > 1)
{
// if (iterationCount < minIterations) goto doneLabel;
Ldloc(iterationCount);
Ldc(minIterations);
BltFar(doneLabel);
}
}
else
{
// The child doesn't backtrack, which means there's no other way the matched iterations could
// match differently, so if we haven't already greedily processed enough iterations, fail the loop.
// if (iterationCount < minIterations)
// {
// if (iterationCount != 0) stackpos = startingStackpos;
// goto originalDoneLabel;
// }
Label enoughIterations = DefineLabel();
Ldloc(iterationCount);
Ldc(minIterations);
Bge(enoughIterations);
// If the minimum iterations is 1, then since we're only here if there are fewer, there must be 0
// iterations, in which case there's nothing to reset. If, however, the minimum iteration count is
// greater than 1, we need to check if there was at least one successful iteration, in which case
// any backtracking state still set needs to be reset; otherwise, constructs earlier in the sequence
// trying to pop their own state will erroneously pop this state instead.
if (minIterations > 1)
{
Debug.Assert(startingStackpos is not null);
Ldloc(iterationCount);
Ldc(0);
BeqFar(originalDoneLabel);
Ldloc(startingStackpos);
Stloc(stackpos);
}
BrFar(originalDoneLabel);
MarkLabel(enoughIterations);
}
}
if (isAtomic)
{
doneLabel = originalDoneLabel;
MarkLabel(endLoop);
// The loop is atomic, which means any backtracking will go around this loop. That also means we can't leave
// stack polluted with state from successful iterations, so we need to remove all such state; such state will
// only have been pushed if minIterations > 0.
if (startingStackpos is not null)
{
Ldloc(startingStackpos);
Stloc(stackpos);
}
}
else
{
if (childBacktracks)
{
// goto endLoop;
BrFar(endLoop);
// Backtrack:
Label backtrack = DefineLabel();
MarkLabel(backtrack);
// We're backtracking. Check the timeout.
EmitTimeoutCheckIfNeeded();
// if (iterationCount == 0) goto originalDoneLabel;
Ldloc(iterationCount);
Ldc(0);
BeqFar(originalDoneLabel);
// goto doneLabel;
BrFar(doneLabel);
doneLabel = backtrack;
}
MarkLabel(endLoop);
// If this loop is itself not in another loop, nothing more needs to be done:
// upon backtracking, locals being used by this loop will have retained their
// values and be up-to-date. But if this loop is inside another loop, multiple
// iterations of this loop each need their own state, so we need to use the stack
// to hold it, and we need a dedicated backtracking section to handle restoring
// that state before jumping back into the loop itself.
if (analysis.IsInLoop(node))
{
// Store the loop's state
EmitStackResizeIfNeeded(1 + (startingPos is not null ? 1 : 0) + (startingStackpos is not null ? 1 : 0));
if (startingPos is not null)
{
EmitStackPush(() => Ldloc(startingPos));
}
if (startingStackpos is not null)
{
EmitStackPush(() => Ldloc(startingStackpos));
}
EmitStackPush(() => Ldloc(iterationCount));
// Skip past the backtracking section
// goto backtrackingEnd;
Label backtrackingEnd = DefineLabel();
BrFar(backtrackingEnd);
// Emit a backtracking section that restores the loop's state and then jumps to the previous done label
Label backtrack = DefineLabel();
MarkLabel(backtrack);
// We're backtracking. Check the timeout.
EmitTimeoutCheckIfNeeded();
// iterationCount = base.runstack[--runstack];
// startingStackpos = base.runstack[--runstack];
// startingPos = base.runstack[--runstack];
EmitStackPop();
Stloc(iterationCount);
if (startingStackpos is not null)
{
EmitStackPop();
Stloc(startingStackpos);
}
if (startingPos is not null)
{
EmitStackPop();
Stloc(startingPos);
}
// goto doneLabel;
BrFar(doneLabel);
doneLabel = backtrack;
MarkLabel(backtrackingEnd);
}
}
}
// <summary>Gets whether an IndexOf expression can be emitted for the node.</summary>
// <param name="node">The RegexNode. If it's a loop, only the one/notone/set aspect of the node is factored in.</param>
// <param name="literalLength">0 if returns false. If it returns true, string.Length for a multi, otherwise 1.</param>
// <returns>true if an IndexOf can be emitted; otherwise, false.</returns>
bool CanEmitIndexOf(RegexNode node, out int literalLength)
{
if (node.Kind == RegexNodeKind.Multi)
{
literalLength = node.Str!.Length;
return true;
}
if (node.IsOneFamily || node.IsNotoneFamily)
{
literalLength = 1;
return true;
}
if (node.IsSetFamily)
{
Span<char> setChars = stackalloc char[128];
if (RegexCharClass.TryGetSingleRange(node.Str, out _, out _) ||
RegexCharClass.GetSetChars(node.Str, setChars) > 0)
{
literalLength = 1;
return true;
}
}
literalLength = 0;
return false;
}
// <summary>Emits the code for IndexOf call based on the node.</summary>
// <param name="node">The RegexNode. If it's a loop, only the one/notone/set aspect of the node is factored in.</param>
// <param name="useLast">true to use LastIndexOf variants; false to use IndexOf variants.</param>
// <param name="negate">true to search for the opposite of the node.</param>
void EmitIndexOf(RegexNode node, bool useLast, bool negate)
{
if (node.Kind == RegexNodeKind.Multi)
{
// IndexOf(span)
Debug.Assert(!negate, "Negation isn't appropriate for a multi");
Ldstr(node.Str!);
Call(StringAsSpanMethod);
Call(useLast ? SpanLastIndexOfSpanMethod : SpanIndexOfSpanMethod);
return;
}
if (node.IsOneFamily || node.IsNotoneFamily)
{
// IndexOf{AnyExcept}(char)
if (node.IsNotoneFamily)
{
negate = !negate;
}
Ldc(node.Ch);
Call((useLast, negate) switch
{
(false, false) => SpanIndexOfCharMethod,
(false, true) => SpanIndexOfAnyExceptCharMethod,
(true, false) => SpanLastIndexOfCharMethod,
(true, true) => SpanLastIndexOfAnyExceptCharMethod,
});
return;
}
if (node.IsSetFamily)
{
bool negated = RegexCharClass.IsNegated(node.Str) ^ negate;
// IndexOfAny{Except}InRange
// Prefer IndexOfAnyInRange over IndexOfAny, except for tiny ranges (1 or 2 items) that IndexOfAny handles more efficiently
if (RegexCharClass.TryGetSingleRange(node.Str, out char lowInclusive, out char highInclusive) &&
(highInclusive - lowInclusive) > 1)
{
Ldc(lowInclusive);
Ldc(highInclusive);
Call((useLast, negated) switch
{
(false, false) => SpanIndexOfAnyInRangeMethod,
(false, true) => SpanIndexOfAnyExceptInRangeMethod,
(true, false) => SpanLastIndexOfAnyInRangeMethod,
(true, true) => SpanLastIndexOfAnyExceptInRangeMethod,
});
return;
}
// IndexOfAny{Except}(ch1, ...)
Span<char> setChars = stackalloc char[128]; // arbitrary cut-off that accomodates all of ASCII and doesn't take too long to compute
int setCharsCount = RegexCharClass.GetSetChars(node.Str, setChars);
if (setCharsCount > 0)
{
setChars = setChars.Slice(0, setCharsCount);
switch (setChars.Length)
{
case 1:
Ldc(setChars[0]);
Call((useLast, negated) switch
{
(false, false) => SpanIndexOfCharMethod,
(false, true) => SpanIndexOfAnyExceptCharMethod,
(true, false) => SpanLastIndexOfCharMethod,
(true, true) => SpanLastIndexOfAnyExceptCharMethod,
});
return;
case 2:
Ldc(setChars[0]);
Ldc(setChars[1]);
Call((useLast, negated) switch
{
(false, false) => SpanIndexOfAnyCharCharMethod,
(false, true) => SpanIndexOfAnyExceptCharCharMethod,
(true, false) => SpanLastIndexOfAnyCharCharMethod,
(true, true) => SpanLastIndexOfAnyExceptCharCharMethod,
});
return;
case 3:
Ldc(setChars[0]);
Ldc(setChars[1]);
Ldc(setChars[2]);
Call((useLast, negated) switch
{
(false, false) => SpanIndexOfAnyCharCharCharMethod,
(false, true) => SpanIndexOfAnyExceptCharCharCharMethod,
(true, false) => SpanLastIndexOfAnyCharCharCharMethod,
(true, true) => SpanLastIndexOfAnyExceptCharCharCharMethod,
});
return;
default:
EmitIndexOfAnyWithSearchValuesOrLiteral(setChars, last: useLast, except: negated);
return;
}
}
}
Debug.Fail("We should never get here. This method should only be called if CanEmitIndexOf returned true, and all of the same cases should be covered.");
}
// <summary>
// If the expression contains captures, pops a crawl position from the stack and uncaptures
// until that position is reached.
// </summary>
void EmitUncaptureUntilPopped()
{
if (expressionHasCaptures)
{
// poppedCrawlPos = base.runstack[--stackpos];
// while (base.Crawlpos() > poppedCrawlPos) base.Uncapture();
using RentedLocalBuilder poppedCrawlPos = RentInt32Local();
EmitStackPop();
Stloc(poppedCrawlPos);
EmitUncaptureUntil(poppedCrawlPos);
}
}
void EmitStackResizeIfNeeded(int count)
{
Debug.Assert(count >= 1);
// if (stackpos >= base.runstack!.Length - (count - 1))
// {
// Array.Resize(ref base.runstack, base.runstack.Length * 2);
// }
Label skipResize = DefineLabel();
Ldloc(stackpos);
Ldthisfld(RunstackField);
Ldlen();
if (count > 1)
{
Ldc(count - 1);
Sub();
}
Blt(skipResize);
Ldthis();
_ilg!.Emit(OpCodes.Ldflda, RunstackField);
Ldthisfld(RunstackField);
Ldlen();
Ldc(2);
Mul();
Call(ArrayResizeMethod);
MarkLabel(skipResize);
}
void EmitStackPush(Action load)
{
// base.runstack[stackpos] = load();
Ldthisfld(RunstackField);
Ldloc(stackpos);
load();
StelemI4();
// stackpos++;
Ldloc(stackpos);
Ldc(1);
Add();
Stloc(stackpos);
}
void EmitStackPop()
{
// ... = base.runstack[--stackpos];
Ldthisfld(RunstackField);
Ldloc(stackpos);
Ldc(1);
Sub();
Stloc(stackpos);
Ldloc(stackpos);
LdelemI4();
}
}
protected void EmitScan(RegexOptions options, MethodInfo tryFindNextStartingPositionMethod, MethodInfo tryMatchAtCurrentPositionMethod)
{
// As with the source generator, we can emit special code for common circumstances rather than always emitting
// the most general purpose scan loop. Unlike the source generator, however, code appearance isn't important
// here, so we don't handle all of the same cases, e.g. we don't special case Empty or Nothing, as they're
// not worth spending any code on.
bool rtl = (options & RegexOptions.RightToLeft) != 0;
RegexNode root = _regexTree!.Root.Child(0);
Label returnLabel = DefineLabel();
if (root.Kind is RegexNodeKind.Multi or RegexNodeKind.One or RegexNodeKind.Notone or RegexNodeKind.Set)
{
// If the whole expression is just one or more characters, we can rely on the FindOptimizations spitting out
// an IndexOf that will find the exact sequence or not, and we don't need to do additional checking beyond that.
// if (!TryFindNextPossibleStartingPosition(inputSpan)) return;
Ldthis();
Ldarg_1();
Call(tryFindNextStartingPositionMethod);
Brfalse(returnLabel);
// int start = base.runtextpos;
LocalBuilder start = DeclareInt32();
Mvfldloc(RuntextposField, start);
// int end = base.runtextpos = start +/- length;
LocalBuilder end = DeclareInt32();
Ldloc(start);
Ldc((root.Kind == RegexNodeKind.Multi ? root.Str!.Length : 1) * (!rtl ? 1 : -1));
Add();
Stloc(end);
Ldthis();
Ldloc(end);
Stfld(RuntextposField);
// base.Capture(0, start, end);
Ldthis();
Ldc(0);
Ldloc(start);
Ldloc(end);
Call(CaptureMethod);
}
else if (_regexTree.FindOptimizations.FindMode is
FindNextStartingPositionMode.LeadingAnchor_LeftToRight_Beginning or
FindNextStartingPositionMode.LeadingAnchor_LeftToRight_Start or
FindNextStartingPositionMode.LeadingAnchor_RightToLeft_Start or
FindNextStartingPositionMode.LeadingAnchor_RightToLeft_End)
{
// If the expression is anchored in such a way that there's one and only one possible position that can match,
// we don't need a scan loop, just a single check and match.
// if (!TryFindNextPossibleStartingPosition(inputSpan)) return;
Ldthis();
Ldarg_1();
Call(tryFindNextStartingPositionMethod);
Brfalse(returnLabel);
// if (TryMatchAtCurrentPosition(inputSpan)) return;
Ldthis();
Ldarg_1();
Call(tryMatchAtCurrentPositionMethod);
Brtrue(returnLabel);
// base.runtextpos = inputSpan.Length; // or 0 for rtl
Ldthis();
if (!rtl)
{
Ldarga_s(1);
Call(SpanGetLengthMethod);
}
else
{
Ldc(0);
}
Stfld(RuntextposField);
}
else
{
// while (TryFindNextPossibleStartingPosition(text))
Label whileLoopBody = DefineLabel();
MarkLabel(whileLoopBody);
Ldthis();
Ldarg_1();
Call(tryFindNextStartingPositionMethod);
BrfalseFar(returnLabel);
// if (TryMatchAtCurrentPosition(text) || runtextpos == text.length) // or == 0 for rtl
// return;
Ldthis();
Ldarg_1();
Call(tryMatchAtCurrentPositionMethod);
BrtrueFar(returnLabel);
Ldthisfld(RuntextposField);
if (!rtl)
{
Ldarga_s(1);
Call(SpanGetLengthMethod);
}
else
{
Ldc(0);
}
Ceq();
BrtrueFar(returnLabel);
// runtextpos++ // or -- for rtl
Ldthis();
Ldthisfld(RuntextposField);
Ldc(!rtl ? 1 : -1);
Add();
Stfld(RuntextposField);
// Check the timeout every time we run the whole match logic at a new starting location, as each such
// operation could do work at least linear in the length of the input.
EmitTimeoutCheckIfNeeded();
// End loop body.
BrFar(whileLoopBody);
}
// return;
MarkLabel(returnLabel);
Ret();
}
/// <summary>Emits a check for whether the character is in the specified character class.</summary>
/// <remarks>The character to be checked has already been loaded onto the stack.</remarks>
private void EmitMatchCharacterClass(string charClass)
{
// We need to perform the equivalent of calling RegexRunner.CharInClass(ch, charClass),
// but that call is relatively expensive. Before we fall back to it, we try to optimize
// some common cases for which we can do much better, such as known character classes
// for which we can call a dedicated method, or a fast-path for ASCII using a lookup table.
// In some cases, multiple optimizations are possible for a given character class: the checks
// in this method are generally ordered from fastest / simplest to slowest / most complex so
// that we get the best optimization for a given char class.
// First, see if the char class is a built-in one for which there's a better function
// we can just call directly.
switch (charClass)
{
case RegexCharClass.AnyClass:
// true
Pop();
Ldc(1);
return;
case RegexCharClass.DigitClass:
case RegexCharClass.NotDigitClass:
// char.IsDigit(ch)
Call(CharIsDigitMethod);
NegateIf(charClass == RegexCharClass.NotDigitClass);
return;
case RegexCharClass.SpaceClass:
case RegexCharClass.NotSpaceClass:
// char.IsWhiteSpace(ch)
Call(CharIsWhiteSpaceMethod);
NegateIf(charClass == RegexCharClass.NotSpaceClass);
return;
case RegexCharClass.WordClass:
case RegexCharClass.NotWordClass:
// RegexRunner.IsWordChar(ch)
Call(IsWordCharMethod);
NegateIf(charClass == RegexCharClass.NotWordClass);
return;
case RegexCharClass.ControlClass:
case RegexCharClass.NotControlClass:
// char.IsControl(ch)
Call(CharIsControlMethod);
NegateIf(charClass == RegexCharClass.NotControlClass);
return;
case RegexCharClass.LetterClass:
case RegexCharClass.NotLetterClass:
// char.IsLetter(ch)
Call(CharIsLetterMethod);
NegateIf(charClass == RegexCharClass.NotLetterClass);
return;
case RegexCharClass.LetterOrDigitClass:
case RegexCharClass.NotLetterOrDigitClass:
// char.IsLetterOrDigit(ch)
Call(CharIsLetterOrDigitMethod);
NegateIf(charClass == RegexCharClass.NotLetterOrDigitClass);
return;
case RegexCharClass.LowerClass:
case RegexCharClass.NotLowerClass:
// char.IsLower(ch)
Call(CharIsLowerMethod);
NegateIf(charClass == RegexCharClass.NotLowerClass);
return;
case RegexCharClass.UpperClass:
case RegexCharClass.NotUpperClass:
// char.IsUpper(ch)
Call(CharIsUpperMethod);
NegateIf(charClass == RegexCharClass.NotUpperClass);
return;
case RegexCharClass.NumberClass:
case RegexCharClass.NotNumberClass:
// char.IsNumber(ch)
Call(CharIsNumberMethod);
NegateIf(charClass == RegexCharClass.NotNumberClass);
return;
case RegexCharClass.PunctuationClass:
case RegexCharClass.NotPunctuationClass:
// char.IsPunctuation(ch)
Call(CharIsPunctuationMethod);
NegateIf(charClass == RegexCharClass.NotPunctuationClass);
return;
case RegexCharClass.SeparatorClass:
case RegexCharClass.NotSeparatorClass:
// char.IsSeparator(ch)
Call(CharIsSeparatorMethod);
NegateIf(charClass == RegexCharClass.NotSeparatorClass);
return;
case RegexCharClass.SymbolClass:
case RegexCharClass.NotSymbolClass:
// char.IsSymbol(ch)
Call(CharIsSymbolMethod);
NegateIf(charClass == RegexCharClass.NotSymbolClass);
return;
case RegexCharClass.AsciiLetterClass:
case RegexCharClass.NotAsciiLetterClass:
// char.IsAsciiLetter(ch)
Call(CharIsAsciiLetterMethod);
NegateIf(charClass == RegexCharClass.NotAsciiLetterClass);
return;
case RegexCharClass.AsciiLetterOrDigitClass:
case RegexCharClass.NotAsciiLetterOrDigitClass:
// char.IsAsciiLetterOrDigit(ch)
Call(CharIsAsciiLetterOrDigitMethod);
NegateIf(charClass == RegexCharClass.NotAsciiLetterOrDigitClass);
return;
case RegexCharClass.HexDigitClass:
case RegexCharClass.NotHexDigitClass:
// char.IsAsciiHexDigit(ch)
Call(CharIsAsciiHexDigitMethod);
NegateIf(charClass == RegexCharClass.NotHexDigitClass);
return;
case RegexCharClass.HexDigitLowerClass:
case RegexCharClass.NotHexDigitLowerClass:
// char.IsAsciiHexDigitLower(ch)
Call(CharIsAsciiHexDigitLowerMethod);
NegateIf(charClass == RegexCharClass.NotHexDigitLowerClass);
return;
case RegexCharClass.HexDigitUpperClass:
case RegexCharClass.NotHexDigitUpperClass:
// char.IsAsciiHexDigitUpper(ch)
Call(CharIsAsciiHexDigitUpperMethod);
NegateIf(charClass == RegexCharClass.NotHexDigitUpperClass);
return;
}
// Next, handle simple sets of one range, e.g. [A-Z], [0-9], etc. This includes some built-in classes, like ECMADigitClass.
if (RegexCharClass.TryGetSingleRange(charClass, out char lowInclusive, out char highInclusive))
{
if (lowInclusive == highInclusive)
{
// ch == charClass[3]
Ldc(lowInclusive);
Ceq();
}
else
{
// (uint)ch - lowInclusive < highInclusive - lowInclusive + 1
Ldc(lowInclusive);
Sub();
Ldc(highInclusive - lowInclusive + 1);
CltUn();
}
// Negate the answer if the negation flag was set
NegateIf(RegexCharClass.IsNegated(charClass));
return;
}
// Next, if the character class contains nothing but Unicode categories, we can call char.GetUnicodeCategory and
// compare against it. It has a fast-lookup path for ASCII, so is as good or better than any lookup we'd generate (plus
// we get smaller code), and it's what we'd do for the fallback (which we get to avoid generating) as part of CharInClass.
// Unlike the source generator, however, we only handle the case of a single UnicodeCategory: the source generator is able
// to rely on C# compiler optimizations to handle dealing with multiple values efficiently.
Span<UnicodeCategory> categories = stackalloc UnicodeCategory[1]; // handle the case of one and only one category
if (RegexCharClass.TryGetOnlyCategories(charClass, categories, out int numCategories, out bool negated))
{
// char.GetUnicodeCategory(ch) == category
Call(CharGetUnicodeInfoMethod);
Ldc((int)categories[0]);
Ceq();
NegateIf(negated);
return;
}
// Checks after this point require reading the input character multiple times,
// so we store it into a temporary local.
using RentedLocalBuilder tempLocal = RentInt32Local();
Stloc(tempLocal);
// Next, if there's only 2 or 3 chars in the set (fairly common due to the sets we create for prefixes),
// it's cheaper and smaller to compare against each than it is to use a lookup table.
Span<char> setChars = stackalloc char[3];
int numChars = RegexCharClass.GetSetChars(charClass, setChars);
if (numChars is 2 or 3)
{
if (RegexCharClass.DifferByOneBit(setChars[0], setChars[1], out int mask)) // special-case common case of an upper and lowercase ASCII letter combination
{
// ((ch | mask) == setChars[1])
Ldloc(tempLocal);
Ldc(mask);
Or();
Ldc(setChars[1] | mask);
Ceq();
}
else
{
// (ch == setChars[0]) | (ch == setChars[1])
Ldloc(tempLocal);
Ldc(setChars[0]);
Ceq();
Ldloc(tempLocal);
Ldc(setChars[1]);
Ceq();
Or();
}
// | (ch == setChars[2])
if (numChars == 3)
{
Ldloc(tempLocal);
Ldc(setChars[2]);
Ceq();
Or();
}
NegateIf(RegexCharClass.IsNegated(charClass));
return;
}
// Next, handle simple sets of two ASCII letter ranges that are cased versions of each other, e.g. [B-Db-d].
// This can be implemented as if it were a single range, with an additional bitwise operation.
if (RegexCharClass.TryGetDoubleRange(charClass, out (char LowInclusive, char HighInclusive) rangeLower, out (char LowInclusive, char HighInclusive) rangeUpper) &&
char.IsAsciiLetter(rangeUpper.LowInclusive) &&
char.IsAsciiLetter(rangeUpper.HighInclusive) &&
(rangeLower.LowInclusive | 0x20) == rangeUpper.LowInclusive &&
(rangeLower.HighInclusive | 0x20) == rangeUpper.HighInclusive)
{
Debug.Assert(rangeLower.LowInclusive != rangeUpper.LowInclusive);
bool negate = RegexCharClass.IsNegated(charClass);
// (uint)((ch | 0x20) - lowInclusive) < highInclusive - lowInclusive + 1
Ldloc(tempLocal);
Ldc(0x20);
Or();
Ldc(rangeUpper.LowInclusive);
Sub();
Ldc(rangeUpper.HighInclusive - rangeUpper.LowInclusive + 1);
CltUn();
NegateIf(negate);
return;
}
// Analyze the character set more to determine what code to generate.
RegexCharClass.CharClassAnalysisResults analysis = RegexCharClass.Analyze(charClass);
// Next, handle sets where the high - low + 1 range is <= 32. In that case, we can emit
// a branchless lookup in a uint that does not rely on loading any objects (e.g. the string-based
// lookup we use later). This nicely handles common sets like [\t\r\n ].
if (analysis.OnlyRanges && (analysis.UpperBoundExclusiveIfOnlyRanges - analysis.LowerBoundInclusiveIfOnlyRanges) <= 32)
{
// Create the 32-bit value with 1s at indices corresponding to every character in the set,
// where the bit is computed to be the char value minus the lower bound starting from
// most significant bit downwards.
uint bitmap = 0;
bool negatedClass = RegexCharClass.IsNegated(charClass);
for (int i = analysis.LowerBoundInclusiveIfOnlyRanges; i < analysis.UpperBoundExclusiveIfOnlyRanges; i++)
{
if (RegexCharClass.CharInClass((char)i, charClass) ^ negatedClass)
{
bitmap |= 1u << (31 - (i - analysis.LowerBoundInclusiveIfOnlyRanges));
}
}
// To determine whether a character is in the set, we subtract the lowest char; this subtraction happens before
// the result is zero-extended to uint, meaning that `charMinusLow` will always have upper 16 bits equal to 0.
// We then left shift the constant with this offset, and apply a bitmask that has the highest bit set (the sign bit)
// if and only if `ch` is in the [low, low + 32) range. Then we only need to check whether this final result is
// less than 0: this will only be the case if both `charMinusLow` was in fact the index of a set bit in the constant,
// and also `ch` was in the allowed range (this ensures that false positive bit shifts are ignored).
// uint charMinusLow = (ushort)(ch - lowInclusive);
LocalBuilder charMinusLow = _ilg!.DeclareLocal(typeof(uint));
Ldloc(tempLocal);
Ldc(analysis.LowerBoundInclusiveIfOnlyRanges);
Sub();
_ilg.Emit(OpCodes.Conv_U2);
Stloc(charMinusLow);
// uint shift = bitmap << (short)charMinusLow;
_ilg.Emit(OpCodes.Ldc_I4, bitmap);
Ldloc(charMinusLow);
_ilg.Emit(OpCodes.Conv_I2);
Ldc(31);
And();
Shl();
// uint mask = charMinusLow - 32;
Ldloc(charMinusLow);
Ldc(32);
_ilg.Emit(OpCodes.Conv_I4);
Sub();
// (int)(shift & mask) < 0 // or >= for a negated character class
And();
Ldc(0);
_ilg.Emit(OpCodes.Conv_I4);
_ilg.Emit(OpCodes.Clt);
NegateIf(negatedClass);
return;
}
// Next, handle sets where the high - low + 1 range is <= 64. As with the 32-bit case above, we can emit
// a branchless lookup in a ulong that does not rely on loading any objects (e.g. the string-based
// lookup we use later). We skip this on 32-bit, as otherwise using 64-bit numbers in this manner is
// a deoptimization when compared to the subsequent fallbacks.
if (IntPtr.Size == 8 && analysis.OnlyRanges && (analysis.UpperBoundExclusiveIfOnlyRanges - analysis.LowerBoundInclusiveIfOnlyRanges) <= 64)
{
// Create the 64-bit value with 1s at indices corresponding to every character in the set,
// where the bit is computed to be the char value minus the lower bound starting from
// most significant bit downwards.
ulong bitmap = 0;
bool negatedClass = RegexCharClass.IsNegated(charClass);
for (int i = analysis.LowerBoundInclusiveIfOnlyRanges; i < analysis.UpperBoundExclusiveIfOnlyRanges; i++)
{
if (RegexCharClass.CharInClass((char)i, charClass) ^ negatedClass)
{
bitmap |= 1ul << (63 - (i - analysis.LowerBoundInclusiveIfOnlyRanges));
}
}
// To determine whether a character is in the set, we subtract the lowest char (casting to
// uint to account for any smaller values); this subtraction happens before the result is
// zero-extended to ulong, meaning that `charMinusLow` will always have upper 32 bits equal to 0.
// We then left shift the constant with this offset, and apply a bitmask that has the highest
// bit set (the sign bit) if and only if `chExpr` is in the [low, low + 64) range.
// Then we only need to check whether this final result is less than 0: this will only be
// the case if both `charMinusLow` was in fact the index of a set bit in the constant, and also
// `chExpr` was in the allowed range (this ensures that false positive bit shifts are ignored).
// ulong charMinusLow = (uint)ch - lowInclusive;
LocalBuilder charMinusLow = _ilg!.DeclareLocal(typeof(ulong));
Ldloc(tempLocal);
Ldc(analysis.LowerBoundInclusiveIfOnlyRanges);
Sub();
_ilg.Emit(OpCodes.Conv_U8);
Stloc(charMinusLow);
// ulong shift = bitmap << (int)charMinusLow;
LdcI8((long)bitmap);
Ldloc(charMinusLow);
_ilg.Emit(OpCodes.Conv_I4);
Ldc(63);
And();
Shl();
// ulong mask = charMinusLow - 64;
Ldloc(charMinusLow);
Ldc(64);
_ilg.Emit(OpCodes.Conv_I8);
Sub();
// (long)(shift & mask) < 0 // or >= for a negated character class
And();
Ldc(0);
_ilg.Emit(OpCodes.Conv_I8);
_ilg.Emit(OpCodes.Clt);
NegateIf(negatedClass);
return;
}
// Next, handle simple sets of two ranges, e.g. [\p{IsGreek}\p{IsGreekExtended}].
if (RegexCharClass.TryGetDoubleRange(charClass, out (char LowInclusive, char HighInclusive) range0, out (char LowInclusive, char HighInclusive) range1))
{
bool negate = RegexCharClass.IsNegated(charClass);
if (range0.LowInclusive == range0.HighInclusive)
{
// ch == lowInclusive
Ldloc(tempLocal);
Ldc(range0.LowInclusive);
Ceq();
}
else
{
// (uint)(ch - lowInclusive) < (uint)(highInclusive - lowInclusive + 1)
Ldloc(tempLocal);
Ldc(range0.LowInclusive);
Sub();
Ldc(range0.HighInclusive - range0.LowInclusive + 1);
CltUn();
}
NegateIf(negate);
if (range1.LowInclusive == range1.HighInclusive)
{
// ch == lowInclusive
Ldloc(tempLocal);
Ldc(range1.LowInclusive);
Ceq();
}
else
{
// (uint)(ch - lowInclusive) < (uint)(highInclusive - lowInclusive + 1)
Ldloc(tempLocal);
Ldc(range1.LowInclusive);
Sub();
Ldc(range1.HighInclusive - range1.LowInclusive + 1);
CltUn();
}
NegateIf(negate);
if (negate)
{
And();
}
else
{
Or();
}
return;
}
using RentedLocalBuilder resultLocal = RentInt32Local();
// Helper method that emits a call to RegexRunner.CharInClass(ch, charClass)
void EmitCharInClass()
{
Ldloc(tempLocal);
Ldstr(charClass);
Call(CharInClassMethod);
Stloc(resultLocal);
}
Label doneLabel = DefineLabel();
Label comparisonLabel = DefineLabel();
void EmitContainsNoAscii()
{
// ch >= 128 && RegexRunner.CharInClass(ch, "...")
Ldloc(tempLocal);
Ldc(128);
Blt(comparisonLabel);
EmitCharInClass();
Br(doneLabel);
MarkLabel(comparisonLabel);
Ldc(0);
Stloc(resultLocal);
MarkLabel(doneLabel);
Ldloc(resultLocal);
}
void EmitAllAsciiContained()
{
// ch < 128 || RegexRunner.CharInClass(ch, "...")
Ldloc(tempLocal);
Ldc(128);
Blt(comparisonLabel);
EmitCharInClass();
Br(doneLabel);
MarkLabel(comparisonLabel);
Ldc(1);
Stloc(resultLocal);
MarkLabel(doneLabel);
Ldloc(resultLocal);
}
if (analysis.ContainsNoAscii)
{
// We determined that the character class contains only non-ASCII,
// for example if the class were [\u1000-\u2000\u3000-\u4000\u5000-\u6000].
// (In the future, we could possibly extend the analysis to produce a known
// lower-bound and compare against that rather than always using 128 as the
// pivot point.)
EmitContainsNoAscii();
return;
}
if (analysis.AllAsciiContained)
{
// We determined that every ASCII character is in the class, for example
// if the class were the negated example from case 1 above:
// [^\p{IsGreek}\p{IsGreekExtended}].
EmitAllAsciiContained();
return;
}
// Now, our big hammer is to generate a lookup table that lets us quickly index by character into a yes/no
// answer as to whether the character is in the target character class. However, we don't want to store
// a lookup table for every possible character for every character class in the regular expression; at one
// bit for each of 65K characters, that would be an 8K bitmap per character class. Instead, we handle the
// common case of ASCII input via such a lookup table, which at one bit for each of 128 characters is only
// 16 bytes per character class. We of course still need to be able to handle inputs that aren't ASCII, so
// we check the input against 128, and have a fallback if the input is >= to it. Determining the right
// fallback could itself be expensive. For example, if it's possible that a value >= 128 could match the
// character class, we output a call to RegexRunner.CharInClass, but we don't want to have to enumerate the
// entire character class evaluating every character against it, just to determine whether it's a match.
// Instead, we employ some quick heuristics that will always ensure we provide a correct answer even if
// we could have sometimes generated better code to give that answer.
// Generate the lookup table to store 128 answers as bits. We use a const string instead of a byte[] / static
// data property because it lets IL emit handle all the details for us.
string bitVectorString = string.Create(8, charClass, static (dest, charClass) => // String length is 8 chars == 16 bytes == 128 bits.
{
for (int i = 0; i < 128; i++)
{
char c = (char)i;
if (RegexCharClass.CharInClass(c, charClass))
{
dest[i >> 4] |= (char)(1 << (i & 0xF));
}
}
});
// There's a chance that the class contains either no ASCII characters or all of them,
// and the analysis could not find it (for example if the class has a subtraction).
// We optimize away the bit vector in these trivial cases.
switch (bitVectorString)
{
case "\0\0\0\0\0\0\0\0":
EmitContainsNoAscii();
return;
case "\uffff\uffff\uffff\uffff\uffff\uffff\uffff\uffff":
EmitAllAsciiContained();
return;
}
// We know that the whole class wasn't ASCII, and we don't know anything about the non-ASCII
// characters other than that some might be included, for example if the character class
// were [\w\d], so if ch >= 128, we need to fall back to calling CharInClass. For ASCII, we
// can use a lookup table, but if it's a known set of ASCII characters we can also use a helper.
// ch < 128 ?
Ldloc(tempLocal);
Ldc(analysis.ContainsOnlyAscii ? analysis.UpperBoundExclusiveIfOnlyRanges : 128);
Bge(comparisonLabel);
// ASCII
switch (bitVectorString)
{
case "\0\0\0\u03ff\ufffe\u07ff\ufffe\u07ff":
// char.IsAsciiLetterOrDigit(ch)
Ldloc(tempLocal);
Call(CharIsAsciiLetterOrDigitMethod);
break;
case "\0\0\0\u03FF\0\0\0\0":
// char.IsAsciiDigit(ch)
Ldloc(tempLocal);
Call(CharIsAsciiDigitMethod);
break;
case "\0\0\0\0\ufffe\u07FF\ufffe\u07ff":
// char.IsAsciiLetter(ch)
Ldloc(tempLocal);
Call(CharIsAsciiLetterMethod);
break;
case "\0\0\0\0\0\0\ufffe\u07ff":
// char.IsAsciiLetterLower(ch)
Ldloc(tempLocal);
Call(CharIsAsciiLetterLowerMethod);
break;
case "\0\0\0\0\ufffe\u07FF\0\0":
// char.IsAsciiLetterUpper(ch)
Ldloc(tempLocal);
Call(CharIsAsciiLetterUpperMethod);
break;
case "\0\0\0\u03FF\u007E\0\u007E\0":
// char.IsAsciiHexDigit(ch)
Ldloc(tempLocal);
Call(CharIsAsciiHexDigitMethod);
break;
case "\0\0\0\u03FF\0\0\u007E\0":
// char.IsAsciiHexDigitLower(ch)
Ldloc(tempLocal);
Call(CharIsAsciiHexDigitLowerMethod);
break;
case "\0\0\0\u03FF\u007E\0\0\0":
// char.IsAsciiHexDigitUpper(ch)
Ldloc(tempLocal);
Call(CharIsAsciiHexDigitUpperMethod);
break;
default:
// (bitVectorString[ch >> 4] & (1 << (ch & 0xF))) != 0
Ldstr(bitVectorString);
Ldloc(tempLocal);
Ldc(4);
Shr();
Call(StringGetCharsMethod);
Ldc(1);
Ldloc(tempLocal);
Ldc(15);
And();
Ldc(31);
And();
Shl();
And();
Ldc(0);
CgtUn();
break;
}
Stloc(resultLocal);
Br(doneLabel);
MarkLabel(comparisonLabel);
// Non-ASCII
if (analysis.ContainsOnlyAscii)
{
// We know that all inputs that could match are ASCII, for example if the
// character class were [A-Za-z0-9], so since the ch is now known to be >= 128, we
// can just fail the comparison.
Ldc(0);
Stloc(resultLocal);
}
else if (analysis.AllNonAsciiContained)
{
// We know that all non-ASCII inputs match, for example if the character
// class were [^\r\n], so since we just determined the ch to be >= 128, we can just
// give back success.
Ldc(1);
Stloc(resultLocal);
}
else
{
// We know that the whole class wasn't ASCII, and we don't know anything about the non-ASCII
// characters other than that some might be included, for example if the character class
// were [\w\d], so since ch >= 128, we need to fall back to calling CharInClass.
EmitCharInClass();
}
MarkLabel(doneLabel);
Ldloc(resultLocal);
}
/// <summary>Emits negation of the value on top of the evaluation stack if <paramref name="condition"/> is true.</summary>
private void NegateIf(bool condition)
{
if (condition)
{
Ldc(0);
Ceq();
}
}
/// <summary>Emits a timeout check if one has been set explicitly or implicitly via a default setting.</summary>
/// Regex timeouts exist to avoid catastrophic backtracking. The goal with timeouts isn't to be accurate to the timeout value,
/// but to ensure that significant backtracking can be stopped. As such, we allow for up to O(n) work in the length of the input
/// between checks, which means we emit checks anywhere backtracking is introduced, such that every check can have O(n) work
/// associated with it. This means checks:
/// - when restarting the whole match evaluation at a new index. Every match could end up doing O(n) work without a timeout
/// check, and since this could then result in O(n) matches, we need a timeout check on each new position in order to
/// avoid O(n^2) work without a timeout check.
/// - when backtracking backwards in a loop. Every backtracking step through the loop could evaluate the remainder of the
/// pattern, which can lead to O(2^n) work if unchecked.
/// - when backtracking forwards in a lazy loop. Every backtracking step through the loop could evaluate the remainder of the
/// pattern, which can lead to O(2^n) work if unchecked.
/// - when backtracking to the next branch of an alternation. Every branch of the alternation could evaluate the remainder of the
/// pattern, which can lead to O(2^n) work if unchecked.
/// - when performing a lookaround. Each lookaround can result in doing O(n) work, which means m lookarounds can result in
/// O(m*n) work. Lookarounds can be in loops, so without timeout checks in a lookaround, a pattern like `((?=(?>a*))a)+`
/// could do O(n^2) work without a timeout check.
/// Note that some other constructs have code that needs to deal with backtracking, e.g. conditionals needing to ensure
/// that if any of their children have backtracking that code which backtracks back into the conditional is appropriately
/// routed to the correct child, but such constructs aren't actually introducing backtracking and thus don't need to be
/// instrumented for timeouts.
private void EmitTimeoutCheckIfNeeded()
{
if (_hasTimeout)
{
// base.CheckTimeout();
Ldthis();
Call(CheckTimeoutMethod);
}
}
/// <summary>Emits a call to either IndexOfAny("abcd") or IndexOfAny(SearchValues) depending on the <paramref name="chars"/>.</summary>
private void EmitIndexOfAnyWithSearchValuesOrLiteral(ReadOnlySpan<char> chars, bool last = false, bool except = false)
{
Debug.Assert(chars.Length > 3, $"chars.Length == {chars.Length}");
// SearchValues<char> is faster than a regular IndexOfAny("abcd") for sets of 4/5 values iff they are ASCII.
// Only emit SearchValues instances when we know they'll be faster to avoid increasing the startup cost too much.
if (chars.Length is 4 or 5 && !RegexCharClass.IsAscii(chars))
{
Ldstr(chars.ToString());
Call(StringAsSpanMethod);
Call((last, except) switch
{
(false, false) => SpanIndexOfAnySpanMethod,
(false, true) => SpanIndexOfAnyExceptSpanMethod,
(true, false) => SpanLastIndexOfAnySpanMethod,
(true, true) => SpanLastIndexOfAnyExceptSpanMethod,
});
}
else
{
LoadSearchValues(chars.ToArray());
Call((last, except) switch
{
(false, false) => SpanIndexOfAnySearchValuesMethod,
(false, true) => SpanIndexOfAnyExceptSearchValuesMethod,
(true, false) => SpanLastIndexOfAnySearchValuesMethod,
(true, true) => SpanLastIndexOfAnyExceptSearchValuesMethod,
});
}
}
/// <summary>
/// Adds an entry in <see cref="CompiledRegexRunner._searchValues"/> for the given <paramref name="values"/> and emits a load of that initialized value.
/// </summary>
/// <param name="values">The values to pass to SearchValues.Create.</param>
/// <param name="comparison">The comparison to pass to SearchValues.Create. Used only when T == string.</param>
private void LoadSearchValues<T>(T[] values, StringComparison comparison = StringComparison.Ordinal)
{
List<object> list = _searchValues ??= new();
int index = list.Count;
Debug.Assert(values is char[] or string[]);
Debug.Assert(comparison is StringComparison.Ordinal || values is string[]);
list.Add(
typeof(T) == typeof(char) ? SearchValues.Create((char[])(object)values) :
typeof(T) == typeof(string) ? SearchValues.Create((string[])(object)values, comparison) :
throw new UnreachableException());
// Logically do _searchValues[index], but avoid the bounds check on accessing the array,
// and cast to the known derived sealed type to enable devirtualization.
// DerivedSearchValues d = Unsafe.As<DerivedSearchValues>(Unsafe.Add(ref MemoryMarshal.GetArrayDataReference(this._searchValues), index));
// ... = d;
Ldthisfld(SearchValuesArrayField);
Call(MemoryMarshalGetArrayDataReferenceSearchValuesMethod);
Ldc(index * IntPtr.Size);
Add();
_ilg!.Emit(OpCodes.Ldind_Ref);
Call(MakeUnsafeAs(list[index].GetType())); // provide JIT with details necessary to devirtualize calls on this instance
[UnconditionalSuppressMessage("ReflectionAnalysis", "IL2060:MakeGenericMethod", Justification =
"Calling Unsafe.As<T> is safe since the T doesn't have trimming annotations.")]
static MethodInfo MakeUnsafeAs(Type type)
{
return UnsafeAsMethod.MakeGenericMethod(type);
}
}
}
}
|