Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New generator apis #74127

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open

New generator apis #74127

wants to merge 7 commits into from

Conversation

chsienki
Copy link
Contributor

Adds APIs to convert ISourceGenerators to IIncrementalGenerators and allows the generator driver to only update a sub-set of generators via a new filter parameter.

Fixes #74039
Fixes #74086

@chsienki chsienki requested review from a team as code owners June 24, 2024 17:42
@dotnet-issue-labeler dotnet-issue-labeler bot added the untriaged Issues and PRs which have not yet been triaged by a lead label Jun 24, 2024
/// <param name="generatorFilter">An optional filter that specifies which generators to run. If <c>null</c> all generators will run.</param>
/// <param name="cancellationToken">Used to cancel an in progress operation.</param>
/// <returns>An updated driver that contains the results of the generators running.</returns>
public GeneratorDriver RunGenerators(Compilation compilation, Func<GeneratorFilterContext, bool>? generatorFilter = null, CancellationToken cancellationToken = default)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't figure out a way to make generatorFilter non-optional (as discussed in the API review) without breaking our back compat guidelines.

Another option would be to just make this a differently named method, like RunGeneratorsWithFilter

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the problem - what guidelines would be broken?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our analyzer requires that we don't have multiple overloads with optional parameters: https://github.com/dotnet/roslyn/blob/main/docs/Adding%20Optional%20Parameters%20in%20Public%20API.md

That means that the signatures when we make generatorFilter required are:

RunGenerators(Compilation, CancellationToken)
RunGenerators(Compilation, Func<> filter, CancellationToken = default)

Anyone who was calling RunGenerators(Compilation) will now be source broken, because we no longer have an overload with only a single required parameter.

Note: It also says that we shouldn't add new optional parameters in the middle of a signature for source-compat. Imagine if you had RunGenerator(compilation, default) that might bind to a different overload now. So we're in a bit of tricky situation.

We can either: make the filter optional, and bend the 'don't put optional in the middle rule', or we can make it non-optional, and introduce a third overload of RunGenerators that only takes a Compilation parameter.

Tagging @333fred for any API shape thoughts on this one?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can make it non-optional, and introduce a third overload of RunGenerators that only takes a Compilation parameter.

This sounds like the best option to me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the problem with the signature in the current form? Yes it's optional but it just gets filled in as essentially "run everything". That behavior is compatible with the existing overload so that all seems to track for me. What am I missing?

@chsienki
Copy link
Contributor Author

@dotnet/roslyn-compiler for review please.

@jaredpar
Copy link
Member

@jjonescz PTAL

/// <param name="generatorFilter">An optional filter that specifies which generators to run. If <c>null</c> all generators will run.</param>
/// <param name="cancellationToken">Used to cancel an in progress operation.</param>
/// <returns>An updated driver that contains the results of the generators running.</returns>
public GeneratorDriver RunGenerators(Compilation compilation, Func<GeneratorFilterContext, bool>? generatorFilter = null, CancellationToken cancellationToken = default)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the problem - what guidelines would be broken?


// step didn't run because the generator was already up to date
Assert.False(stepRan);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RegisterPostInitializationOutput should run even if the generator is filtered out, right? Consider testing that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, this is tricky. It seems logical to say that we shouldn't even init a generator if it's filtered out, which would skip the post init stage.

However, we also have a step there where we re-parse the post init docs if the parse options change. Right now that's a single flag as it's assumed all the generators run. If we skip a generator on a parse options change then it'll not get them reparsed and cause errors the next time we run.

We would need to track per-generator if they have had their parse options updated or not, so that we can re-parse them as needed. The alternative is to just always do the init + postinit steps for a generator even if its requested to be skipped.

Let me play around a little and see how hard the per-generator flag would be.

Copy link
Contributor Author

@chsienki chsienki Jun 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, the per generator flag is actually trivial, as we can just compare the produced trees with the latest parse options.

I've updated it so that the generator doesn't init, and doesn't take part in the compilation at all if it's filtered out + added tests to cover those cases.

Add tests to ensure we re-parse init only trees if the parse options changed between filtered runs
@@ -47,7 +47,7 @@ public static Type GetGeneratorType(this IIncrementalGenerator generator)
/// Converts an <see cref="IIncrementalGenerator"/> into an <see cref="ISourceGenerator"/> object that can be used when constructing a <see cref="GeneratorDriver"/>
/// </summary>
/// <param name="incrementalGenerator">The incremental generator to wrap</param>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// <param name="incrementalGenerator">The incremental generator to wrap</param>
/// <param name="incrementalGenerator">The incremental generator to convert</param>

@@ -124,5 +125,7 @@ public GeneratorState WithError(Exception exception, Diagnostic error, TimeSpan
internal ImmutableDictionary<string, ImmutableArray<IncrementalGeneratorRunStep>> OutputSteps { get; }

internal ImmutableArray<(string Key, string Value)> HostOutputs { get; }

internal bool RequiresPostInitReparse(ParseOptions parseOptions) => PostInitTrees.Any(t => t.Tree.Options != parseOptions);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using static lambda if possible:

Suggested change
internal bool RequiresPostInitReparse(ParseOptions parseOptions) => PostInitTrees.Any(t => t.Tree.Options != parseOptions);
internal bool RequiresPostInitReparse(ParseOptions parseOptions) => PostInitTrees.Any(static (t, parseOptions) => t.Tree.Options != parseOptions, parseOptions);

@@ -20,8 +20,7 @@ namespace Microsoft.CodeAnalysis
DriverStateTable stateTable,
SyntaxStore syntaxStore,
GeneratorDriverOptions driverOptions,
TimeSpan runtime,
bool parseOptionsChanged)
TimeSpan runtime)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove the internal readonly bool ParseOptionsChanged; field as well?

/// A dummy extension that is used to indicate this adaptor was created outside of the driver.
/// </summary>
public const string DummySourceExtension = ".dummy";

private readonly string _sourceExtension;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a value that is controllable by customers? If so have reservations about using a magic string. Had trouble tracking this through all the APIs to see where it originates.

/// <param name="generatorFilter">An optional filter that specifies which generators to run. If <c>null</c> all generators will run.</param>
/// <param name="cancellationToken">Used to cancel an in progress operation.</param>
/// <returns>An updated driver that contains the results of the generators running.</returns>
public GeneratorDriver RunGenerators(Compilation compilation, Func<GeneratorFilterContext, bool>? generatorFilter = null, CancellationToken cancellationToken = default)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the problem with the signature in the current form? Yes it's optional but it just gets filled in as essentially "run everything". That behavior is compatible with the existing overload so that all seems to track for me. What am I missing?

@@ -195,7 +205,7 @@ public GeneratorDriverTimingInfo GetTimingInfo()
return new GeneratorDriverTimingInfo(_state.RunTime, generatorTimings);
}

internal GeneratorDriverState RunGeneratorsCore(Compilation compilation, DiagnosticBag? diagnosticsBag, CancellationToken cancellationToken = default)
internal GeneratorDriverState RunGeneratorsCore(Compilation compilation, DiagnosticBag? diagnosticsBag, Func<GeneratorFilterContext, bool>? generatorFilter = null, CancellationToken cancellationToken = default)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than having the filter be optional here why not normalize it in the constructor to be non-null? Essentially somethnig like

generatorFilter ??= static _ => true;

Believe that should be amortized to a single alloc, if not could cache as a static readonly on the type

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area-Compilers New Feature - Source Generators Source Generators untriaged Issues and PRs which have not yet been triaged by a lead
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Run a single generator at a time .AsIncrementalGenerator extension method
4 participants