diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexAssemblyCompiler.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexAssemblyCompiler.cs index 01aa572b2217a..1d40cf67920fb 100644 --- a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexAssemblyCompiler.cs +++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexAssemblyCompiler.cs @@ -48,7 +48,7 @@ internal void GenerateRegexType(string pattern, RegexOptions options, string nam _strings = code.Strings; _leadingCharClasses = code.LeadingCharClasses; _boyerMoorePrefix = code.BoyerMoorePrefix; - _anchors = code.Anchors; + _leadingAnchor = code.LeadingAnchor; _trackcount = code.TrackCount; // Pick a name for the class. diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexCode.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexCode.cs index 4682b20f30292..8df7e0676361a 100644 --- a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexCode.cs +++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexCode.cs @@ -103,14 +103,14 @@ internal sealed class RegexCode public readonly (string CharClass, bool CaseInsensitive)[]? LeadingCharClasses; // the set of candidate first characters, if available. Each entry corresponds to the next char in the input. public int[]? LeadingCharClassAsciiLookup; // the ASCII lookup table optimization for LeadingCharClasses[0], if it exists; only used by the interpreter public readonly RegexBoyerMoore? BoyerMoorePrefix; // the fixed prefix string as a Boyer-Moore machine, if available - public readonly int Anchors; // the set of zero-length start anchors (RegexPrefixAnalyzer.Bol, etc) + public readonly int LeadingAnchor; // the leading anchor, if one exists (RegexPrefixAnalyzer.Bol, etc) public readonly bool RightToLeft; // true if right to left public RegexCode(RegexTree tree, int[] codes, string[] strings, int trackcount, Hashtable? caps, int capsize, RegexBoyerMoore? boyerMoorePrefix, (string CharClass, bool CaseInsensitive)[]? leadingCharClasses, - int anchors, bool rightToLeft) + int leadingAnchor, bool rightToLeft) { Debug.Assert(boyerMoorePrefix is null || leadingCharClasses is null); @@ -123,7 +123,7 @@ public RegexCode(RegexTree tree, int[] codes, string[] strings, int trackcount, CapSize = capsize; BoyerMoorePrefix = boyerMoorePrefix; LeadingCharClasses = leadingCharClasses; - Anchors = anchors; + LeadingAnchor = leadingAnchor; RightToLeft = rightToLeft; } @@ -402,7 +402,7 @@ public override string ToString() var sb = new StringBuilder(); sb.AppendLine("Direction: " + (RightToLeft ? "right-to-left" : "left-to-right")); - sb.AppendLine("Anchors: " + RegexPrefixAnalyzer.AnchorDescription(Anchors)); + sb.AppendLine("Anchor: " + RegexPrefixAnalyzer.AnchorDescription(LeadingAnchor)); sb.AppendLine(""); if (BoyerMoorePrefix != null) diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexCompiler.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexCompiler.cs index 7fe6d91760668..4f07dfca18786 100644 --- a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexCompiler.cs +++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexCompiler.cs @@ -67,6 +67,7 @@ internal abstract class RegexCompiler private static readonly MethodInfo s_stringAsSpanMethod = typeof(MemoryExtensions).GetMethod("AsSpan", new Type[] { typeof(string) })!; private static readonly MethodInfo s_stringAsSpanIntIntMethod = typeof(MemoryExtensions).GetMethod("AsSpan", new Type[] { typeof(string), typeof(int), typeof(int) })!; private static readonly MethodInfo s_stringGetCharsMethod = typeof(string).GetMethod("get_Chars", new Type[] { typeof(int) })!; + private static readonly MethodInfo s_stringIndexOfCharInt = typeof(string).GetMethod("IndexOf", new Type[] { typeof(char), typeof(int) })!; /// /// The max recursion depth used for computations that can recover for not walking the entire node tree. @@ -99,7 +100,7 @@ internal abstract class RegexCompiler protected string[]? _strings; // the stringtable associated with the RegexCodes protected (string CharClass, bool CaseInsensitive)[]? _leadingCharClasses; // the possible first chars computed by RegexPrefixAnalyzer protected RegexBoyerMoore? _boyerMoorePrefix; // a prefix as a boyer-moore machine - protected int _anchors; // the set of anchors + protected int _leadingAnchor; // the set of anchors protected bool _hasTimeout; // whether the regex has a non-infinite timeout private Label[]? _labels; // a label for every operation in _codes @@ -1025,122 +1026,174 @@ protected void GenerateFindFirstChar() MarkLabel(finishedLengthCheck); // Generate anchor checks. - if ((_anchors & (RegexPrefixAnalyzer.Beginning | RegexPrefixAnalyzer.Start | RegexPrefixAnalyzer.EndZ | RegexPrefixAnalyzer.End)) != 0) + if ((_leadingAnchor & (RegexPrefixAnalyzer.Beginning | RegexPrefixAnalyzer.Start | RegexPrefixAnalyzer.EndZ | RegexPrefixAnalyzer.End | RegexPrefixAnalyzer.Bol)) != 0) { - if (!_code.RightToLeft) + switch (_leadingAnchor) { - if ((_anchors & RegexPrefixAnalyzer.Beginning) != 0) - { - Label l1 = DefineLabel(); - Ldloc(_runtextposLocal); - Ldthisfld(s_runtextbegField); - Ble(l1); - Br(returnFalse); - MarkLabel(l1); - } - - if ((_anchors & RegexPrefixAnalyzer.Start) != 0) - { - Label l1 = DefineLabel(); - Ldloc(_runtextposLocal); - Ldthisfld(s_runtextstartField); - Ble(l1); - BrFar(returnFalse); - MarkLabel(l1); - } + case RegexPrefixAnalyzer.Beginning: + { + Label l1 = DefineLabel(); + Ldloc(_runtextposLocal); + if (!_code.RightToLeft) + { + Ldthisfld(s_runtextbegField); + Ble(l1); + Br(returnFalse); + } + else + { + Ldloc(_runtextbegLocal!); + Ble(l1); + Ldthis(); + Ldloc(_runtextbegLocal!); + Stfld(s_runtextposField); + } + MarkLabel(l1); + } + Ldc(1); + Ret(); + return; - if ((_anchors & RegexPrefixAnalyzer.EndZ) != 0) - { - Label l1 = DefineLabel(); - Ldloc(_runtextposLocal); - Ldloc(_runtextendLocal); + case RegexPrefixAnalyzer.Start: + { + Label l1 = DefineLabel(); + Ldloc(_runtextposLocal); + Ldthisfld(s_runtextstartField); + if (!_code.RightToLeft) + { + Ble(l1); + } + else + { + Bge(l1); + } + Br(returnFalse); + MarkLabel(l1); + } Ldc(1); - Sub(); - Bge(l1); - Ldthis(); - Ldloc(_runtextendLocal); + Ret(); + return; + + case RegexPrefixAnalyzer.EndZ: + { + Label l1 = DefineLabel(); + if (!_code.RightToLeft) + { + Ldloc(_runtextposLocal); + Ldloc(_runtextendLocal); + Ldc(1); + Sub(); + Bge(l1); + Ldthis(); + Ldloc(_runtextendLocal); + Ldc(1); + Sub(); + Stfld(s_runtextposField); + MarkLabel(l1); + } + else + { + Label l2 = DefineLabel(); + Ldloc(_runtextposLocal); + Ldloc(_runtextendLocal); + Ldc(1); + Sub(); + Blt(l1); + Ldloc(_runtextposLocal); + Ldloc(_runtextendLocal); + Beq(l2); + Ldthisfld(s_runtextField); + Ldloc(_runtextposLocal); + Callvirt(s_stringGetCharsMethod); + Ldc('\n'); + Beq(l2); + MarkLabel(l1); + BrFar(returnFalse); + MarkLabel(l2); + } + } Ldc(1); - Sub(); - Stfld(s_runtextposField); - MarkLabel(l1); - } + Ret(); + return; - if ((_anchors & RegexPrefixAnalyzer.End) != 0) - { - if (minRequiredLength == 0) // if it's > 0, we already output a more stringent check + case RegexPrefixAnalyzer.End when minRequiredLength == 0: // if it's > 0, we already output a more stringent check { Label l1 = DefineLabel(); Ldloc(_runtextposLocal); Ldloc(_runtextendLocal); - Bge(l1); - Ldthis(); - Ldloc(_runtextendLocal); - Stfld(s_runtextposField); + if (!_code.RightToLeft) + { + Bge(l1); + Ldthis(); + Ldloc(_runtextendLocal); + Stfld(s_runtextposField); + } + else + { + Bge(l1); + Br(returnFalse); + } MarkLabel(l1); } - } - } - else - { - if ((_anchors & RegexPrefixAnalyzer.End) != 0) - { - Label l1 = DefineLabel(); - Ldloc(_runtextposLocal); - Ldloc(_runtextendLocal); - Bge(l1); - Br(returnFalse); - MarkLabel(l1); - } - - if ((_anchors & RegexPrefixAnalyzer.EndZ) != 0) - { - Label l1 = DefineLabel(); - Label l2 = DefineLabel(); - Ldloc(_runtextposLocal); - Ldloc(_runtextendLocal); Ldc(1); - Sub(); - Blt(l1); - Ldloc(_runtextposLocal); - Ldloc(_runtextendLocal); - Beq(l2); - Ldthisfld(s_runtextField); - Ldloc(_runtextposLocal); - Callvirt(s_stringGetCharsMethod); - Ldc('\n'); - Beq(l2); - MarkLabel(l1); - BrFar(returnFalse); - MarkLabel(l2); - } + Ret(); + return; - if ((_anchors & RegexPrefixAnalyzer.Start) != 0) - { - Label l1 = DefineLabel(); - Ldloc(_runtextposLocal); - Ldthisfld(s_runtextstartField); - Bge(l1); - BrFar(returnFalse); - MarkLabel(l1); - } + case RegexPrefixAnalyzer.Bol when !_code.RightToLeft: // don't bother optimizing for the niche case of RegexOptions.RightToLeft | RegexOptions.Multiline + { + // 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 Boyer-Moore or + // leading char class searches. - if ((_anchors & RegexPrefixAnalyzer.Beginning) != 0) - { - Label l1 = DefineLabel(); - Ldloc(_runtextposLocal); - Ldloc(_runtextbegLocal!); - Ble(l1); - Ldthis(); - Ldloc(_runtextbegLocal!); - Stfld(s_runtextposField); - MarkLabel(l1); - } - } + Label atBeginningOfLine = DefineLabel(); - Ldc(1); - Ret(); + // if (runtextpos > runtextbeg... + Ldloc(_runtextposLocal!); + Ldthisfld(s_runtextbegField); + BleFar(atBeginningOfLine); + + // ... && runtext[runtextpos - 1] != '\n') { ... } + Ldthisfld(s_runtextField); + Ldloc(_runtextposLocal); + Ldc(1); + Sub(); + Callvirt(s_stringGetCharsMethod); + Ldc('\n'); + BeqFar(atBeginningOfLine); + + // int tmp = runtext.IndexOf('\n', runtextpos); + Ldthisfld(s_runtextField); + Ldc('\n'); + Ldloc(_runtextposLocal); + Call(s_stringIndexOfCharInt); + Dup(); + + // if (tmp == -1) + // { + // runtextpos = runtextend; + // return false; + // } + Label foundNextLine = DefineLabel(); + Ldc(-1); + BneFar(foundNextLine); + Pop(); + BrFar(returnFalse); + + // runtextpos = tmp + 1; + MarkLabel(foundNextLine); + Ldc(1); + Add(); + Stloc(_runtextposLocal); + + MarkLabel(atBeginningOfLine); + } + break; + } } - else if (_boyerMoorePrefix != null && _boyerMoorePrefix.NegativeUnicode == null) + + if (_boyerMoorePrefix != null && _boyerMoorePrefix.NegativeUnicode == null) { // Compiled Boyer-Moore string matching LocalBuilder chLocal = _temp1Local; diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexInterpreter.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexInterpreter.cs index 3354d49706919..2de366b9a34f8 100644 --- a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexInterpreter.cs +++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexInterpreter.cs @@ -352,48 +352,69 @@ protected override bool FindFirstChar() // If the pattern is anchored, we can update our position appropriately and return immediately. // If there's a Boyer-Moore prefix, we can also validate it. - if ((_code.Anchors & (RegexPrefixAnalyzer.Beginning | RegexPrefixAnalyzer.Start | RegexPrefixAnalyzer.EndZ | RegexPrefixAnalyzer.End)) != 0) + if ((_code.LeadingAnchor & (RegexPrefixAnalyzer.Beginning | RegexPrefixAnalyzer.Start | RegexPrefixAnalyzer.EndZ | RegexPrefixAnalyzer.End)) != 0) { if (!_code.RightToLeft) { - if (((_code.Anchors & RegexPrefixAnalyzer.Beginning) != 0 && runtextpos > runtextbeg) || - ((_code.Anchors & RegexPrefixAnalyzer.Start) != 0 && runtextpos > runtextstart)) + switch (_code.LeadingAnchor) { - runtextpos = runtextend; - return false; - } + case RegexPrefixAnalyzer.Beginning when runtextpos > runtextbeg: + case RegexPrefixAnalyzer.Start when runtextpos > runtextstart: + runtextpos = runtextend; + return false; - if ((_code.Anchors & RegexPrefixAnalyzer.EndZ) != 0 && runtextpos < runtextend - 1) - { - runtextpos = runtextend - 1; - } - else if ((_code.Anchors & RegexPrefixAnalyzer.End) != 0 && runtextpos < runtextend) - { - runtextpos = runtextend; + case RegexPrefixAnalyzer.EndZ when runtextpos < runtextend - 1: + runtextpos = runtextend - 1; + break; + + case RegexPrefixAnalyzer.End when runtextpos < runtextend: + runtextpos = runtextend; + break; } } else { - if (((_code.Anchors & RegexPrefixAnalyzer.End) != 0 && runtextpos < runtextend) || - ((_code.Anchors & RegexPrefixAnalyzer.EndZ) != 0 && (runtextpos < runtextend - 1 || (runtextpos == runtextend - 1 && runtext![runtextpos] != '\n'))) || - ((_code.Anchors & RegexPrefixAnalyzer.Start) != 0 && runtextpos < runtextstart)) + switch (_code.LeadingAnchor) { - runtextpos = runtextbeg; - return false; + case RegexPrefixAnalyzer.End when runtextpos < runtextend: + case RegexPrefixAnalyzer.EndZ when runtextpos < runtextend - 1 || (runtextpos == runtextend - 1 && runtext![runtextpos] != '\n'): + case RegexPrefixAnalyzer.Start when runtextpos < runtextstart: + runtextpos = runtextbeg; + return false; + + case RegexPrefixAnalyzer.Beginning when runtextpos > runtextbeg: + runtextpos = runtextbeg; + break; } + } + + return + _code.BoyerMoorePrefix == null || // found a valid start or end anchor + _code.BoyerMoorePrefix.IsMatch(runtext!, runtextpos, runtextbeg, runtextend); + } - if ((_code.Anchors & RegexPrefixAnalyzer.Beginning) != 0 && runtextpos > runtextbeg) + // 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 Boyer-Moore or + // leading char class searches. + if (_code.LeadingAnchor == RegexPrefixAnalyzer.Bol && + !_code.RightToLeft) // don't bother customizing this optimization for the very niche RTL + Multiline case + { + // If we're not currently positioned at the beginning of a line (either + // the beginning of the string or just after a line feed), find the next + // newline and position just after it. + if (runtextpos > runtextbeg && runtext![runtextpos - 1] != '\n') + { + int newline = runtext.IndexOf('\n', runtextpos); + if (newline == -1) { - runtextpos = runtextbeg; + runtextpos = runtextend; + return false; } - } - if (_code.BoyerMoorePrefix != null) - { - return _code.BoyerMoorePrefix.IsMatch(runtext!, runtextpos, runtextbeg, runtextend); + runtextpos = newline + 1; } - - return true; // found a valid start or end anchor } if (_code.BoyerMoorePrefix != null) diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexLWCGCompiler.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexLWCGCompiler.cs index cf6462a2694f9..7f3fa4001a797 100644 --- a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexLWCGCompiler.cs +++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexLWCGCompiler.cs @@ -42,7 +42,7 @@ public RegexRunnerFactory FactoryInstanceFromCode(string pattern, RegexCode code _strings = code.Strings; _leadingCharClasses = code.LeadingCharClasses; _boyerMoorePrefix = code.BoyerMoorePrefix; - _anchors = code.Anchors; + _leadingAnchor = code.LeadingAnchor; _trackcount = code.TrackCount; _options = options; _hasTimeout = hasTimeout; diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexPrefixAnalyzer.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexPrefixAnalyzer.cs index b06125b1597f7..627a4850c1ca6 100644 --- a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexPrefixAnalyzer.cs +++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexPrefixAnalyzer.cs @@ -290,7 +290,7 @@ public static (string CharClass, bool CaseInsensitive)[]? ComputeMultipleCharCla } /// Takes a RegexTree and computes the leading anchor that it encounters. - public static int FindLeadingAnchors(RegexTree tree) + public static int FindLeadingAnchor(RegexTree tree) { RegexNode curNode = tree.Root; RegexNode? concatNode = null; @@ -300,6 +300,30 @@ public static int FindLeadingAnchors(RegexTree tree) { switch (curNode.Type) { + case RegexNode.Bol: + return Bol; + + case RegexNode.Eol: + return Eol; + + case RegexNode.Boundary: + return Boundary; + + case RegexNode.ECMABoundary: + return ECMABoundary; + + case RegexNode.Beginning: + return Beginning; + + case RegexNode.Start: + return Start; + + case RegexNode.EndZ: + return EndZ; + + case RegexNode.End: + return End; + case RegexNode.Concatenate: if (curNode.ChildCount() > 0) { @@ -314,27 +338,6 @@ public static int FindLeadingAnchors(RegexTree tree) concatNode = null; continue; - case RegexNode.Bol: - case RegexNode.Eol: - case RegexNode.Boundary: - case RegexNode.ECMABoundary: - case RegexNode.Beginning: - case RegexNode.Start: - case RegexNode.EndZ: - case RegexNode.End: - return curNode.Type switch - { - RegexNode.Bol => Bol, - RegexNode.Eol => Eol, - RegexNode.Boundary => Boundary, - RegexNode.ECMABoundary => ECMABoundary, - RegexNode.Beginning => Beginning, - RegexNode.Start => Start, - RegexNode.EndZ => EndZ, - RegexNode.End => End, - _ => 0, - }; - case RegexNode.Empty: case RegexNode.Require: case RegexNode.Prevent: diff --git a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexWriter.cs b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexWriter.cs index 32500886400d4..33fe44fec090b 100644 --- a/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexWriter.cs +++ b/src/libraries/System.Text.RegularExpressions/src/System/Text/RegularExpressions/RegexWriter.cs @@ -170,7 +170,7 @@ public RegexCode RegexCodeFromRegexTree(RegexTree tree) } // Compute any anchors starting the expression. - int anchors = RegexPrefixAnalyzer.FindLeadingAnchors(tree); + int leadingAnchor = RegexPrefixAnalyzer.FindLeadingAnchor(tree); // Convert the string table into an ordered string array. var strings = new string[_stringTable.Count]; @@ -180,7 +180,7 @@ public RegexCode RegexCodeFromRegexTree(RegexTree tree) } // Return all that in a RegexCode object. - return new RegexCode(tree, emitted, strings, _trackCount, _caps, capsize, boyerMoorePrefix, leadingCharClasses, anchors, rtl); + return new RegexCode(tree, emitted, strings, _trackCount, _caps, capsize, boyerMoorePrefix, leadingCharClasses, leadingAnchor, rtl); } /// diff --git a/src/libraries/System.Text.RegularExpressions/tests/Regex.MultipleMatches.Tests.cs b/src/libraries/System.Text.RegularExpressions/tests/Regex.MultipleMatches.Tests.cs index be6a8f1af3ce8..0726794e564b6 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/Regex.MultipleMatches.Tests.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/Regex.MultipleMatches.Tests.cs @@ -192,6 +192,66 @@ public static IEnumerable Matches_TestData() } }; + // Using ^ with multiline + yield return new object[] + { + "^", "", RegexOptions.Multiline, + new[] { new CaptureData("", 0, 0) } + }; + yield return new object[] + { + "^", "\n\n\n", RegexOptions.Multiline, + new[] + { + new CaptureData("", 0, 0), + new CaptureData("", 1, 0), + new CaptureData("", 2, 0), + new CaptureData("", 3, 0) + } + }; + yield return new object[] + { + "^abc", "abc\nabc \ndef abc \nab\nabc", RegexOptions.Multiline, + new[] + { + new CaptureData("abc", 0, 3), + new CaptureData("abc", 4, 3), + new CaptureData("abc", 21, 3), + } + }; + yield return new object[] + { + @"^\w{5}", "abc\ndefg\n\nhijkl\n", RegexOptions.Multiline, + new[] + { + new CaptureData("hijkl", 10, 5), + } + }; + yield return new object[] + { + @"^.*$", "abc\ndefg\n\nhijkl\n", RegexOptions.Multiline, + new[] + { + new CaptureData("abc", 0, 3), + new CaptureData("defg", 4, 4), + new CaptureData("", 9, 0), + new CaptureData("hijkl", 10, 5), + new CaptureData("", 16, 0), + } + }; + yield return new object[] + { + @"^.*$", "abc\ndefg\n\nhijkl\n", RegexOptions.Multiline | RegexOptions.RightToLeft, + new[] + { + new CaptureData("", 16, 0), + new CaptureData("hijkl", 10, 5), + new CaptureData("", 9, 0), + new CaptureData("defg", 4, 4), + new CaptureData("abc", 0, 3), + } + }; + if (!PlatformDetection.IsNetFramework) { // .NET Framework missing fix in https://github.com/dotnet/runtime/pull/1075