Skip to content

Commit 850a8ea

Browse files
Refine Regex generator Results to ImmutableEquatableArray without diagnostics/locations
- Remove unrelated base.runtextpos change from RegexGenerator.Emitter.cs - Move DiagnosticLocation, Tree, Analysis out of RegexMethod positional params so they don't participate in record equality - Convert helpers Dictionary to ImmutableEquatableArray for value equality - Build ImmutableEquatableArray<ValueTuple<...>> in Collect().Select() that contains only data needed for source emission (no Diagnostic or Location) - Add ImmutableEquatableArray.cs and HashHelpers.cs to generator csproj Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 8f9bbcd commit 850a8ea

File tree

4 files changed

+77
-56
lines changed

4 files changed

+77
-56
lines changed

src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Emitter.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1106,7 +1106,6 @@ bool EmitAnchors()
11061106
noMatchFoundLabelNeeded = true;
11071107
Goto(NoMatchFound);
11081108
}
1109-
writer.WriteLine("base.runtextpos = pos;");
11101109
}
11111110
writer.WriteLine();
11121111
break;

src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.Parser.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -214,15 +214,15 @@ regexPropertySymbol.SetMethod is not null ||
214214
var result = new RegexPatternAndSyntax(
215215
regexType,
216216
IsProperty: regexMemberSymbol is IPropertySymbol,
217-
memberSyntax.GetLocation(),
218217
regexMemberSymbol.Name,
219218
memberSyntax.Modifiers.ToString(),
220219
nullableRegex,
221220
pattern,
222221
regexOptions,
223222
matchTimeout,
224223
culture,
225-
compilationData);
224+
compilationData)
225+
{ DiagnosticLocation = memberSyntax.GetLocation() };
226226

227227
RegexType current = regexType;
228228
var parent = typeDec.Parent as TypeDeclarationSyntax;
@@ -249,11 +249,21 @@ SyntaxKind.RecordStructDeclaration or
249249
}
250250

251251
/// <summary>Data about a regex directly from the GeneratedRegex attribute.</summary>
252-
internal sealed record RegexPatternAndSyntax(RegexType DeclaringType, bool IsProperty, Location DiagnosticLocation, string MemberName, string Modifiers, bool NullableRegex, string Pattern, RegexOptions Options, int? MatchTimeout, CultureInfo Culture, CompilationData CompilationData);
252+
internal sealed record RegexPatternAndSyntax(RegexType DeclaringType, bool IsProperty, string MemberName, string Modifiers, bool NullableRegex, string Pattern, RegexOptions Options, int? MatchTimeout, CultureInfo Culture, CompilationData CompilationData)
253+
{
254+
/// <summary>Location for diagnostic reporting. Not part of record equality.</summary>
255+
public Location DiagnosticLocation { get; init; } = Location.None;
256+
}
253257

254258
/// <summary>Data about a regex, including a fully parsed RegexTree and subsequent analysis.</summary>
255-
internal sealed record RegexMethod(RegexType DeclaringType, bool IsProperty, Location DiagnosticLocation, string MemberName, string Modifiers, bool NullableRegex, string Pattern, RegexOptions Options, int? MatchTimeout, RegexTree Tree, AnalysisResults Analysis, CompilationData CompilationData)
259+
internal sealed record RegexMethod(RegexType DeclaringType, bool IsProperty, string MemberName, string Modifiers, bool NullableRegex, string Pattern, RegexOptions Options, int? MatchTimeout, CompilationData CompilationData)
256260
{
261+
/// <summary>Location for diagnostic reporting. Not part of record equality.</summary>
262+
public Location DiagnosticLocation { get; init; } = Location.None;
263+
/// <summary>Parsed regex tree. Not part of record equality.</summary>
264+
public RegexTree Tree { get; init; } = null!;
265+
/// <summary>Analysis results from the regex tree. Not part of record equality.</summary>
266+
public AnalysisResults Analysis { get; init; } = null!;
257267
public string? GeneratedName { get; set; }
258268
public bool IsDuplicate { get; set; }
259269
}

src/libraries/System.Text.RegularExpressions/gen/RegexGenerator.cs

Lines changed: 61 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Microsoft.CodeAnalysis;
1212
using Microsoft.CodeAnalysis.CSharp;
1313
using Microsoft.CodeAnalysis.CSharp.Syntax;
14+
using SourceGenerators;
1415

1516
[assembly: System.Resources.NeutralResourcesLanguage("en-us")]
1617

@@ -77,7 +78,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
7778
{
7879
RegexTree regexTree = RegexParser.Parse(method.Pattern, method.Options | RegexOptions.Compiled, method.Culture); // make sure Compiled is included to get all optimizations applied to it
7980
AnalysisResults analysis = RegexTreeAnalyzer.Analyze(regexTree);
80-
return new RegexMethod(method.DeclaringType, method.IsProperty, method.DiagnosticLocation, method.MemberName, method.Modifiers, method.NullableRegex, method.Pattern, method.Options, method.MatchTimeout, regexTree, analysis, method.CompilationData);
81+
return new RegexMethod(method.DeclaringType, method.IsProperty, method.MemberName, method.Modifiers, method.NullableRegex, method.Pattern, method.Options, method.MatchTimeout, method.CompilationData)
82+
{
83+
DiagnosticLocation = method.DiagnosticLocation,
84+
Tree = regexTree,
85+
Analysis = analysis,
86+
};
8187
}
8288
catch (Exception e)
8389
{
@@ -101,7 +107,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
101107
// We'll still output a limited implementation that just caches a new Regex(...).
102108
if (!SupportsCodeGeneration(regexMethod, regexMethod.CompilationData.LanguageVersion, out string? reason))
103109
{
104-
return (regexMethod, reason, Diagnostic.Create(DiagnosticDescriptors.LimitedSourceGeneration, regexMethod.DiagnosticLocation), regexMethod.CompilationData);
110+
return (object)(
111+
regexMethod,
112+
(string?)null,
113+
(string?)reason,
114+
ImmutableEquatableArray<(string, ImmutableEquatableArray<string>)>.Empty,
115+
(Diagnostic?)Diagnostic.Create(DiagnosticDescriptors.LimitedSourceGeneration, regexMethod.DiagnosticLocation));
105116
}
106117

107118
// Generate the core logic for the regex.
@@ -112,35 +123,51 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
112123
writer.WriteLine();
113124
EmitRegexDerivedTypeRunnerFactory(writer, regexMethod, requiredHelpers, regexMethod.CompilationData.CheckOverflow);
114125
writer.Indent -= 2;
115-
return (regexMethod, sw.ToString(), requiredHelpers, regexMethod.CompilationData);
126+
127+
// Convert helpers to an equatable form for the incremental pipeline.
128+
var equatableHelpers = requiredHelpers
129+
.OrderBy(h => h.Key, StringComparer.Ordinal)
130+
.Select(h => (h.Key, h.Value.ToImmutableEquatableArray()))
131+
.ToImmutableEquatableArray();
132+
133+
return (object)(regexMethod, (string?)sw.ToString(), (string?)null, equatableHelpers, (Diagnostic?)null);
116134
})
117135

118136
// Combine all of the generated text outputs into a single batch, then split
119137
// the source model from diagnostics so they can be emitted independently.
138+
// The Results use ImmutableEquatableArray for deep value equality, ensuring the
139+
// source output callback only fires when emitted code would actually change.
120140
.Collect()
121141
.Select(static (results, _) =>
122142
{
123143
ImmutableArray<Diagnostic>.Builder? diagnostics = null;
144+
var methods = new List<(RegexMethod Method, string? RunnerFactory, string? LimitedReason, ImmutableEquatableArray<(string Key, ImmutableEquatableArray<string> Lines)> Helpers)>();
124145

125146
foreach (object result in results)
126147
{
127148
if (result is Diagnostic d)
128149
{
129150
(diagnostics ??= ImmutableArray.CreateBuilder<Diagnostic>()).Add(d);
130151
}
131-
else if (result is ValueTuple<RegexMethod, string, Diagnostic, CompilationData> limitedSupportResult)
152+
else if (result is ValueTuple<RegexMethod, string, string, ImmutableEquatableArray<(string, ImmutableEquatableArray<string>)>, Diagnostic> entry)
132153
{
133-
(diagnostics ??= ImmutableArray.CreateBuilder<Diagnostic>()).Add(limitedSupportResult.Item3);
154+
methods.Add((entry.Item1, entry.Item2, entry.Item3, entry.Item4));
155+
if (entry.Item5 is Diagnostic diag)
156+
{
157+
(diagnostics ??= ImmutableArray.CreateBuilder<Diagnostic>()).Add(diag);
158+
}
134159
}
135160
}
136161

137-
return (Results: results, Diagnostics: diagnostics?.ToImmutable() ?? ImmutableArray<Diagnostic>.Empty);
162+
return (
163+
Results: methods.ToImmutableEquatableArray(),
164+
Diagnostics: diagnostics?.ToImmutable() ?? ImmutableArray<Diagnostic>.Empty);
138165
});
139166

140-
// Project to just the source model for code generation.
167+
// Project to just the equatable source model for code generation.
141168
context.RegisterSourceOutput(collected.Select(static (t, _) => t.Results), static (context, results) =>
142169
{
143-
if (results.All(static r => r is Diagnostic))
170+
if (results.Count == 0)
144171
{
145172
return;
146173
}
@@ -170,48 +197,34 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
170197
// pair is the implementation used for the key.
171198
var emittedExpressions = new Dictionary<(string Pattern, RegexOptions Options, int? Timeout), RegexMethod>();
172199

173-
// If we have any (RegexMethod regexMethod, string generatedName, string reason, Diagnostic diagnostic), these are regexes for which we have
174-
// limited support and need to simply output boilerplate.
175-
// If we have any (RegexMethod regexMethod, string generatedName, string runnerFactoryImplementation, Dictionary<string, string[]> requiredHelpers),
176-
// those are generated implementations to be emitted. We need to gather up their required helpers.
177-
Dictionary<string, string[]> requiredHelpers = new();
178-
foreach (object? result in results)
200+
// Gather required helpers from all regex methods with full implementations.
201+
var requiredHelpers = new Dictionary<string, ImmutableEquatableArray<string>>();
202+
foreach ((RegexMethod method, string? runnerFactory, string? limitedReason, ImmutableEquatableArray<(string Key, ImmutableEquatableArray<string> Lines)> helpers) in results)
179203
{
180-
RegexMethod? regexMethod = null;
181-
if (result is ValueTuple<RegexMethod, string, Diagnostic, CompilationData> limitedSupportResult)
204+
var key = (method.Pattern, method.Options, method.MatchTimeout);
205+
if (emittedExpressions.TryGetValue(key, out RegexMethod? implementation))
182206
{
183-
regexMethod = limitedSupportResult.Item1;
207+
method.IsDuplicate = true;
208+
method.GeneratedName = implementation.GeneratedName;
184209
}
185-
else if (result is ValueTuple<RegexMethod, string, Dictionary<string, string[]>, CompilationData> regexImpl)
210+
else
186211
{
187-
foreach (KeyValuePair<string, string[]> helper in regexImpl.Item3)
188-
{
189-
if (!requiredHelpers.ContainsKey(helper.Key))
190-
{
191-
requiredHelpers.Add(helper.Key, helper.Value);
192-
}
193-
}
194-
195-
regexMethod = regexImpl.Item1;
212+
method.IsDuplicate = false;
213+
method.GeneratedName = $"{method.MemberName}_{id++}";
214+
emittedExpressions.Add(key, method);
196215
}
197216

198-
if (regexMethod is not null)
217+
EmitRegexPartialMethod(method, writer);
218+
writer.WriteLine();
219+
220+
foreach ((string helperKey, ImmutableEquatableArray<string> helperLines) in helpers)
199221
{
200-
var key = (regexMethod.Pattern, regexMethod.Options, regexMethod.MatchTimeout);
201-
if (emittedExpressions.TryGetValue(key, out RegexMethod? implementation))
202-
{
203-
regexMethod.IsDuplicate = true;
204-
regexMethod.GeneratedName = implementation.GeneratedName;
205-
}
206-
else
222+
#pragma warning disable CA1864 // Prefer Dictionary.TryAdd -- not available on netstandard2.0
223+
if (!requiredHelpers.ContainsKey(helperKey))
207224
{
208-
regexMethod.IsDuplicate = false;
209-
regexMethod.GeneratedName = $"{regexMethod.MemberName}_{id++}";
210-
emittedExpressions.Add(key, regexMethod);
225+
requiredHelpers.Add(helperKey, helperLines);
211226
}
212-
213-
EmitRegexPartialMethod(regexMethod, writer);
214-
writer.WriteLine();
227+
#pragma warning restore CA1864
215228
}
216229
}
217230

@@ -237,21 +250,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
237250

238251
// Emit each Regex-derived type.
239252
writer.Indent++;
240-
foreach (object? result in results)
253+
foreach ((RegexMethod method, string? runnerFactory, string? limitedReason, _) in results)
241254
{
242-
if (result is ValueTuple<RegexMethod, string, Diagnostic, CompilationData> limitedSupportResult)
255+
if (!method.IsDuplicate)
243256
{
244-
if (!limitedSupportResult.Item1.IsDuplicate)
257+
if (limitedReason is not null)
245258
{
246-
EmitRegexLimitedBoilerplate(writer, limitedSupportResult.Item1, limitedSupportResult.Item2, limitedSupportResult.Item4.LanguageVersion);
259+
EmitRegexLimitedBoilerplate(writer, method, limitedReason, method.CompilationData.LanguageVersion);
247260
writer.WriteLine();
248261
}
249-
}
250-
else if (result is ValueTuple<RegexMethod, string, Dictionary<string, string[]>, CompilationData> regexImpl)
251-
{
252-
if (!regexImpl.Item1.IsDuplicate)
262+
else if (runnerFactory is not null)
253263
{
254-
EmitRegexDerivedImplementation(writer, regexImpl.Item1, regexImpl.Item2, regexImpl.Item4.AllowUnsafe);
264+
EmitRegexDerivedImplementation(writer, method, runnerFactory, method.CompilationData.AllowUnsafe);
255265
writer.WriteLine();
256266
}
257267
}
@@ -268,7 +278,7 @@ public void Initialize(IncrementalGeneratorInitializationContext context)
268278
writer.WriteLine($"{{");
269279
writer.Indent++;
270280
bool sawFirst = false;
271-
foreach (KeyValuePair<string, string[]> helper in requiredHelpers.OrderBy(h => h.Key, StringComparer.Ordinal))
281+
foreach (KeyValuePair<string, ImmutableEquatableArray<string>> helper in requiredHelpers.OrderBy(h => h.Key, StringComparer.Ordinal))
272282
{
273283
if (sawFirst)
274284
{

src/libraries/System.Text.RegularExpressions/gen/System.Text.RegularExpressions.Generator.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
<Compile Include="$(CommonPath)Roslyn\DiagnosticDescriptorHelper.cs" Link="Common\Roslyn\DiagnosticDescriptorHelper.cs" />
2727
<Compile Include="$(CommonPath)Roslyn\GetBestTypeByMetadataName.cs" Link="Common\Roslyn\GetBestTypeByMetadataName.cs" />
2828
<Compile Include="$(CoreLibSharedDir)System\Runtime\CompilerServices\IsExternalInit.cs" Link="Common\System\Runtime\CompilerServices\IsExternalInit.cs" />
29+
<Compile Include="$(CoreLibSharedDir)System\Numerics\Hashing\HashHelpers.cs" Link="Common\System\Numerics\Hashing\HashHelpers.cs" />
30+
<Compile Include="$(CommonPath)SourceGenerators\ImmutableEquatableArray.cs" Link="Common\SourceGenerators\ImmutableEquatableArray.cs" />
2931

3032
<!-- Code included from System.Text.RegularExpressions -->
3133
<Compile Include="$(CommonPath)System\HexConverter.cs" Link="Production\HexConverter.cs" />

0 commit comments

Comments
 (0)